From eac98dbbd69954573bea3671aebc91938639f2f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jan 2021 07:29:40 +0100 Subject: [PATCH 0001/1386] Version bump to 2021.1 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index e96e7f530..74c8c412c 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = 'develop' +__version__ = '2021.1' if __version__ == 'develop': From 69d62ef38316df7aa33683badbcef6590253ac76 Mon Sep 17 00:00:00 2001 From: Eko Aprili Trisno Date: Thu, 4 Feb 2021 01:06:52 +0700 Subject: [PATCH 0002/1386] Add Refresh / Reload Button on rpc/Telegram --- freqtrade/rpc/telegram.py | 99 +++++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 99f9a8a91..ad72e10e4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -11,9 +11,9 @@ from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update +from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.error import NetworkError, TelegramError -from telegram.ext import CallbackContext, CommandHandler, Updater +from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ @@ -40,9 +40,13 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: update = kwargs.get('update') or args[0] # Reject unauthorized messages + if update.callback_query: + cchat_id = int(update.callback_query.message.chat.id) + else: + cchat_id = int(update.message.chat_id) + chat_id = int(self._config['telegram']['chat_id']) - - if int(update.message.chat_id) != chat_id: + if cchat_id != chat_id: logger.info( 'Rejected unauthorized message from: %s', update.message.chat_id @@ -150,10 +154,22 @@ class Telegram(RPCHandler): CommandHandler('logs', self._logs), CommandHandler('edge', self._edge), CommandHandler('help', self._help), - CommandHandler('version', self._version), + CommandHandler('version', self._version) + ] + callbacks = [ + CallbackQueryHandler(self._status_table, pattern='update_status_table'), + CallbackQueryHandler(self._daily, pattern='update_daily'), + CallbackQueryHandler(self._profit, pattern='update_profit'), + CallbackQueryHandler(self._profit, pattern='update_balance'), + CallbackQueryHandler(self._profit, pattern='update_performance'), + CallbackQueryHandler(self._profit, pattern='update_count') ] for handle in handles: self._updater.dispatcher.add_handler(handle) + + for handle in callbacks: + self._updater.dispatcher.add_handler(handle) + self._updater.start_polling( clean=True, bootstrap_retries=-1, @@ -336,9 +352,12 @@ class Telegram(RPCHandler): try: statlist, head = self._rpc._rpc_status_table( self._config['stake_currency'], self._config.get('fiat_display_currency', '')) - message = tabulate(statlist, headers=head, tablefmt='simple') - self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) + if(update.callback_query): + query = update.callback_query + self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, msg=f"
{message}
", parse_mode=ParseMode.HTML, callback_path="update_status_table", reload_able=True) + else: + self._send_msg(f"
{message}
", reload_able=True, callback_path="update_status_table", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @@ -376,7 +395,11 @@ class Telegram(RPCHandler): ], tablefmt='simple') message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' - self._send_msg(message, parse_mode=ParseMode.HTML) + if(update.callback_query): + query = update.callback_query + self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, msg=message, parse_mode=ParseMode.HTML, callback_path="update_daily", reload_able=True) + else: + self._send_msg(msg=message, parse_mode=ParseMode.HTML, callback_path="update_daily", reload_able=True) except RPCException as e: self._send_msg(str(e)) @@ -435,7 +458,11 @@ class Telegram(RPCHandler): if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") - self._send_msg(markdown_msg) + if(update.callback_query): + query = update.callback_query + self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, msg=markdown_msg, callback_path="update_profit", reload_able=True) + else: + self._send_msg(msg=markdown_msg, callback_path="update_profit", reload_able=True) @authorized_only def _stats(self, update: Update, context: CallbackContext) -> None: @@ -514,7 +541,11 @@ class Telegram(RPCHandler): output += ("\n*Estimated Value*:\n" "\t`{stake}: {total: .8f}`\n" "\t`{symbol}: {value: .2f}`\n").format(**result) - self._send_msg(output) + if(update.callback_query): + query = update.callback_query + self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, msg=output, callback_path="update_balance", reload_able=True) + else: + self._send_msg(msg=output, callback_path="update_balance", reload_able=True) except RPCException as e: self._send_msg(str(e)) @@ -679,7 +710,11 @@ class Telegram(RPCHandler): count=trade['count'] ) for i, trade in enumerate(trades)) message = 'Performance:\n{}'.format(stats) - self._send_msg(message, parse_mode=ParseMode.HTML) + if(update.callback_query): + query = update.callback_query + self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, msg=message, parse_mode=ParseMode.HTML, callback_path="update_performance", reload_able=True) + else: + self._send_msg(msg=message, parse_mode=ParseMode.HTML, callback_path="update_performance", reload_able=True) except RPCException as e: self._send_msg(str(e)) @@ -699,7 +734,11 @@ class Telegram(RPCHandler): tablefmt='simple') message = "
{}
".format(message) logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + if(update.callback_query): + query = update.callback_query + self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, msg=message, parse_mode=ParseMode.HTML, callback_path="update_count", reload_able=True) + else: + self._send_msg(msg=message, parse_mode=ParseMode.HTML, callback_path="update_count", reload_able=True) except RPCException as e: self._send_msg(str(e)) @@ -901,8 +940,35 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) - def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False) -> None: + def _update_msg(self, chat_id: str, message_id: str, msg: str, callback_path: str = "", reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: + if reload_able: + reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("Refresh", callback_data=callback_path)]]) + else: + reply_markup = InlineKeyboardMarkup([[]]) + try: + try: + self._updater.bot.edit_message_text( + chat_id=chat_id, + message_id=message_id, + text=msg, + parse_mode=parse_mode, + reply_markup=reply_markup + ) + except BadRequest as e: + if 'not modified' in e.message.lower(): + pass + else: + logger.warning( + 'TelegramError: %s', + e.message + ) + except TelegramError as telegram_err: + logger.warning( + 'TelegramError: %s! Giving up on that message.', + telegram_err.message + ) + + def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, callback_path: str = "", reload_able: bool = False) -> None: """ Send given markdown message :param msg: message @@ -910,7 +976,10 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ - reply_markup = ReplyKeyboardMarkup(self._keyboard) + if reload_able: + reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("Refresh", callback_data=callback_path)]]) + else: + reply_markup = ReplyKeyboardMarkup(self._keyboard) try: try: self._updater.bot.send_message( From 21d3635e8dcf9f41d241cb628534a8376f37fb1c Mon Sep 17 00:00:00 2001 From: Eko Aprili Trisno Date: Thu, 4 Feb 2021 01:16:27 +0700 Subject: [PATCH 0003/1386] Update telegram.py --- freqtrade/rpc/telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ad72e10e4..32af71a76 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -160,9 +160,9 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._status_table, pattern='update_status_table'), CallbackQueryHandler(self._daily, pattern='update_daily'), CallbackQueryHandler(self._profit, pattern='update_profit'), - CallbackQueryHandler(self._profit, pattern='update_balance'), - CallbackQueryHandler(self._profit, pattern='update_performance'), - CallbackQueryHandler(self._profit, pattern='update_count') + CallbackQueryHandler(self._balance, pattern='update_balance'), + CallbackQueryHandler(self._performance, pattern='update_performance'), + CallbackQueryHandler(self._count, pattern='update_count') ] for handle in handles: self._updater.dispatcher.add_handler(handle) From 54d0ac9d20d077214c8df3b568b57f5114bb97da Mon Sep 17 00:00:00 2001 From: Eko Aprili Trisno Date: Thu, 4 Feb 2021 01:19:23 +0700 Subject: [PATCH 0004/1386] Update telegram.py --- 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 32af71a76..7cb05b04a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,7 +12,7 @@ from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.error import NetworkError, TelegramError +from telegram.error import NetworkError, TelegramError, BadRequest from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler from telegram.utils.helpers import escape_markdown From ba32708ed44091b9eb2bc15ac8eeedfce98f3fd1 Mon Sep 17 00:00:00 2001 From: Eko Aprili Trisno Date: Sun, 14 Feb 2021 01:40:04 +0700 Subject: [PATCH 0005/1386] Update telegram.py --- freqtrade/rpc/telegram.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7cb05b04a..80d7be60f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,14 +5,14 @@ This module manage Telegram communication """ import json import logging -from datetime import timedelta +from datetime import timedelta, datetime from itertools import chain from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.error import NetworkError, TelegramError, BadRequest +from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler from telegram.utils.helpers import escape_markdown @@ -154,7 +154,7 @@ class Telegram(RPCHandler): CommandHandler('logs', self._logs), CommandHandler('edge', self._edge), CommandHandler('help', self._help), - CommandHandler('version', self._version) + CommandHandler('version', self._version), ] callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), @@ -945,6 +945,7 @@ class Telegram(RPCHandler): reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("Refresh", callback_data=callback_path)]]) else: reply_markup = InlineKeyboardMarkup([[]]) + msg+="\nUpdated: {}".format(datetime.now().ctime()) try: try: self._updater.bot.edit_message_text( @@ -976,10 +977,10 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ - if reload_able: + if reload_able and self._config['telegram'].get('reload',True): reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("Refresh", callback_data=callback_path)]]) else: - reply_markup = ReplyKeyboardMarkup(self._keyboard) + reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( From aea8f05d10946ca488a2f1ec2564e92d0922feff Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Feb 2021 06:39:59 +0100 Subject: [PATCH 0006/1386] Version bump 2021.2 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 74c8c412c..2205d284d 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.1' +__version__ = '2021.2' if __version__ == 'develop': From 834f00f5803431270f736d3a098eb9c5623cfbe8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Feb 2021 06:46:07 +0100 Subject: [PATCH 0007/1386] Refresh slack link --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/developer.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afa41ed33..c29d6e632 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Few pointers for contributions: - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). -If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index 7ef0d4ce7..c3a665c47 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) @@ -178,7 +178,7 @@ to understand the requirements before sending your pull-requests. Coding is not a necessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `stable`. diff --git a/docs/developer.md b/docs/developer.md index c09e528bf..4b8c64530 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) where you can ask questions. ## Documentation diff --git a/docs/faq.md b/docs/faq.md index 87b0893bd..93b806dca 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -142,7 +142,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: diff --git a/docs/index.md b/docs/index.md index db5088707..9d1a1532e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -81,7 +81,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). ## Ready to try? From e92441643157652467eed377a43d057d32c18708 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Sun, 14 Mar 2021 22:02:53 +0900 Subject: [PATCH 0008/1386] correct math used in examples and clarify some terminology regarding custom stoploss functions --- docs/strategy-advanced.md | 64 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 56061365e..cda988acd 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -71,12 +71,13 @@ See `custom_stoploss` examples below on how to access the saved dataframe column ## Custom stoploss -A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. -Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. +The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. -The method must return a stoploss value (float / number) with a relative ratio below the current price. -E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). +The method must return a stoploss value (float / number) as a percentage of the current price. +E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD. + +The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: @@ -177,16 +178,33 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` + +#### Calculating stoploss relative to open price + +Stoploss values returned from `custom_stoploss` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +This can be calculated as: + +``` python +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + +``` + +For example, say our open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, 0.21)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + #### Trailing stoploss with positive offset Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. -Please note that the stoploss can only increase, values lower than the current stoploss are ignored. ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -197,28 +215,32 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: - return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss + return 1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - desired_stoploss = current_profit / 2 - # Use a minimum of 2.5% and a maximum of 5% - return max(min(desired_stoploss, 0.05), 0.025) + desired_stop_from_open = max(min(current_profit / 2, 0.05), 0.025) + + return stoploss_from_open(desired_stop_from_open, current_profit) ``` -#### Absolute stoploss +#### Stepped stoploss -The below example sets absolute profit levels based on the current profit. +Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. * Use the regular stoploss until 20% profit is reached -* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. -* Once profit is > 25% - stoploss will be 15%. -* Once profit is > 20% - stoploss will be set to 7%. +* Once profit is > 20% - set stoploss to 7% above open price. +* Once profit is > 25% - set stoploss to 15% above open price. +* Once profit is > 40% - set stoploss to 25% above open price. + ``` python from datetime import datetime from freqtrade.persistence import Trade +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -228,13 +250,15 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price + # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return (-0.25 + current_profit) - if current_profit > 0.25: - return (-0.15 + current_profit) - if current_profit > 0.20: - return (-0.07 + current_profit) + return stoploss_from_open(0.25, current_profit) + elif current_profit > 0.25: + return stoploss_from_open(0.15, current_profit) + elif current_profit > 0.20: + return stoploss_from_open(0.07, current_profit) + + # return maximum stoploss value, keeping current stoploss price unchanged return 1 ``` #### Custom stoploss using an indicator from dataframe example From 79d4585dadf14e7e3749cabb498ef8cfe47f99eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Mar 2021 19:24:03 +0100 Subject: [PATCH 0009/1386] Add check to ensure close_profit_abs is filled on closed trades Technically, this should not be possible, but #4554 shows it is. closes #4554 --- freqtrade/wallets.py | 3 ++- tests/test_persistence.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 575fe1b67..f4432e932 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -71,7 +71,8 @@ class Wallets: # TODO: potentially remove the ._log workaround to determine backtest mode. if self._log: closed_trades = Trade.get_trades_proxy(is_open=False) - tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) + tot_profit = sum( + [trade.close_profit_abs for trade in closed_trades if trade.close_profit_abs]) else: tot_profit = LocalTrade.total_profit tot_in_trades = sum([trade.stake_amount for trade in open_trades]) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index ab900cbb8..1820250a5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1066,8 +1066,14 @@ def test_get_trades_proxy(fee, use_db): assert isinstance(trades[0], Trade) - assert len(Trade.get_trades_proxy(is_open=True)) == 4 - assert len(Trade.get_trades_proxy(is_open=False)) == 2 + trades = Trade.get_trades_proxy(is_open=True) + assert len(trades) == 4 + assert trades[0].is_open + trades = Trade.get_trades_proxy(is_open=False) + + assert len(trades) == 2 + assert not trades[0].is_open + opendate = datetime.now(tz=timezone.utc) - timedelta(minutes=15) assert len(Trade.get_trades_proxy(open_date=opendate)) == 3 From aee2591490b442a731c8efb039658e8230958cd5 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Wed, 17 Mar 2021 17:58:23 +0900 Subject: [PATCH 0010/1386] add stoploss_from_open() as a strategy_helper --- docs/strategy-customization.md | 36 +++++++++++++++++++++++++++ freqtrade/strategy/__init__.py | 1 + freqtrade/strategy/strategy_helper.py | 18 ++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a1708a481..bf086bc0a 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -587,6 +587,42 @@ All columns of the informative dataframe will be available on the returning data *** +### *stoploss_from_open()* + +Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price. + +??? Example "Returning a stoploss relative to the open price from the custom stoploss function" + + Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). + + If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + + + ``` python + + from freqtrade.strategy import IStrategy, stoploss_from_open + from datetime import datetime + from freqtrade.persistence import Trade + + class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + # once the profit has risin above 10%, keep the stoploss at 7% above the open price + if current_profit > 0.10: + return stoploss_from_open(0.07, current_profit) + + return 1 + + ``` + + + ## Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 662156ae9..3de90666e 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,3 +3,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair +from freqtrade.strategy.strategy_helper import stoploss_from_open diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index d7b1327d9..f40fa285d 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -56,3 +56,21 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, dataframe = dataframe.ffill() return dataframe + + +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + """ + + Given the current profit, and a desired stop loss value relative to the open price, + return a stop loss value that is relative to the current price, and which can be + returned from `custom_stoploss`. + + :param open_relative_stop: Desired stop loss value relative to open price + :param current_profit: The current profit percentage + :return: Stop loss value relative to current price + """ + + if current_profit == -1: + return 1 + + return 1-((1+open_relative_stop)/(1+current_profit)) From ce1ed76269370862f6f717c4d9ffe98b049d7caa Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Wed, 17 Mar 2021 22:44:10 +0900 Subject: [PATCH 0011/1386] complete stoploss_from_open and associated test --- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/strategy_helper.py | 15 ++++++++-- tests/strategy/test_strategy_helpers.py | 39 ++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 3de90666e..85148b6ea 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,5 +2,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pair -from freqtrade.strategy.strategy_helper import stoploss_from_open +from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index f40fa285d..22b6f0be5 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -65,12 +65,21 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa return a stop loss value that is relative to the current price, and which can be returned from `custom_stoploss`. - :param open_relative_stop: Desired stop loss value relative to open price + The requested stop can be positive for a stop above the open price, or negative for + a stop below the open price. The return value is always >= 0. + + Returns 0 if the resulting stop price would be above the current price. + + :param open_relative_stop: Desired stop loss percentage relative to open price :param current_profit: The current profit percentage - :return: Stop loss value relative to current price + :return: Positive stop loss value relative to current price """ + # formula is undefined for current_profit -1, return maximum value if current_profit == -1: return 1 - return 1-((1+open_relative_stop)/(1+current_profit)) + stoploss = 1-((1+open_relative_stop)/(1+current_profit)) + + # negative stoploss values indicate the requested stop price is higher than the current price + return max(stoploss, 0.0) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 252288e2e..3b84fc254 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -1,8 +1,10 @@ +from math import isclose + import numpy as np import pandas as pd import pytest -from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes +from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes def generate_test_data(timeframe: str, size: int): @@ -95,3 +97,38 @@ def test_merge_informative_pair_lower(): with pytest.raises(ValueError, match=r"Tried to merge a faster timeframe .*"): merge_informative_pair(data, informative, '1h', '15m', ffill=True) + + +def test_stoploss_from_open(): + open_price_ranges = [ + [0.01, 1.00, 30], + [1, 100, 30], + [100, 10000, 30], + ] + current_profit_range = [-0.99, 2, 30] + desired_stop_range = [-0.50, 0.50, 30] + + for open_range in open_price_ranges: + for open_price in np.linspace(*open_range): + for desired_stop in np.linspace(*desired_stop_range): + + # -1 is not a valid current_profit, should return 1 + assert stoploss_from_open(desired_stop, -1) == 1 + + for current_profit in np.linspace(*current_profit_range): + current_price = open_price * (1 + current_profit) + expected_stop_price = open_price * (1 + desired_stop) + + stoploss = stoploss_from_open(desired_stop, current_profit) + + assert stoploss >= 0 + assert stoploss <= 1 + + stop_price = current_price * (1 - stoploss) + + # there is no correct answer if the expected stop price is above + # the current price + if expected_stop_price > current_price: + assert stoploss == 0 + else: + assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) From 6597055a24f5592057f62ec330e72dda310a606c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 19:36:11 +0100 Subject: [PATCH 0012/1386] Ensure ccxt tests run without dry-run closes #4566 --- tests/exchange/test_ccxt_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 03cb30d62..870e6cabd 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -44,6 +44,7 @@ EXCHANGES = { def exchange_conf(): config = get_default_conf((Path(__file__).parent / "testdata").resolve()) config['exchange']['pair_whitelist'] = [] + config['dry_run'] = False return config From b05de6d4687299f5012aec4eeebc0ed8dbebb173 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 19:36:35 +0100 Subject: [PATCH 0013/1386] Move advanced exchange config to exchange page --- docs/configuration.md | 20 -------------------- docs/exchanges.md | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 2e8edca2e..ca1e03b0a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -417,26 +417,6 @@ This configuration enables binance, as well as rate limiting to avoid bans from Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. -#### Advanced Freqtrade Exchange configuration - -Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours. - -Available options are listed in the exchange-class as `_ft_has_default`. - -For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call): - -```json -"exchange": { - "name": "kraken", - "_ft_has_params": { - "order_time_in_force": ["gtc", "fok"], - "ohlcv_candle_limit": 200 - } -``` - -!!! Warning - Please make sure to fully understand the impacts of these settings before modifying them. - ### What values can be used for fiat_display_currency? The `fiat_display_currency` configuration parameter sets the base currency to use for the diff --git a/docs/exchanges.md b/docs/exchanges.md index 2e5bdfadd..4c7e44b06 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -118,3 +118,23 @@ Whether your exchange returns incomplete candles or not can be checked using [th Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle. However, if it is based on the need for the latest price for your strategy - then this requirement can be acquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy. + +### Advanced Freqtrade Exchange configuration + +Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behavior. + +Available options are listed in the exchange-class as `_ft_has_default`. + +For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call): + +```json +"exchange": { + "name": "kraken", + "_ft_has_params": { + "order_time_in_force": ["gtc", "fok"], + "ohlcv_candle_limit": 200 + } +``` + +!!! Warning + Please make sure to fully understand the impacts of these settings before modifying them. From 76ca3c219f9d7b7ccf8a2723267529acbf54f658 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 20:43:51 +0100 Subject: [PATCH 0014/1386] extract result-printing from hyperopt class --- freqtrade/commands/hyperopt_commands.py | 21 +- freqtrade/optimize/hyperopt.py | 290 +---------------------- freqtrade/optimize/hyperopt_tools.py | 294 ++++++++++++++++++++++++ tests/commands/test_commands.py | 4 +- tests/optimize/test_hyperopt.py | 19 +- 5 files changed, 324 insertions(+), 304 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_tools.py diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index fd8f737f0..268e3eeef 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -17,7 +17,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: """ List hyperopt epochs previously evaluated """ - from freqtrade.optimize.hyperopt import Hyperopt + from freqtrade.optimize.hyperopt_tools import HyperoptTools config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -47,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: config.get('hyperoptexportfilename')) # Previous evaluations - epochs = Hyperopt.load_previous_results(results_file) + epochs = HyperoptTools.load_previous_results(results_file) total_epochs = len(epochs) epochs = hyperopt_filter_epochs(epochs, filteroptions) @@ -57,18 +57,19 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if not export_csv: try: - print(Hyperopt.get_result_table(config, epochs, total_epochs, - not filteroptions['only_best'], print_colorized, 0)) + print(HyperoptTools.get_result_table(config, epochs, total_epochs, + not filteroptions['only_best'], + print_colorized, 0)) except KeyboardInterrupt: print('User interrupted..') if epochs and not no_details: sorted_epochs = sorted(epochs, key=itemgetter('loss')) results = sorted_epochs[0] - Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header) + HyperoptTools.print_epoch_details(results, total_epochs, print_json, no_header) if epochs and export_csv: - Hyperopt.export_csv_file( + HyperoptTools.export_csv_file( config, epochs, total_epochs, not filteroptions['only_best'], export_csv ) @@ -77,7 +78,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: """ Show details of a hyperopt epoch previously evaluated """ - from freqtrade.optimize.hyperopt import Hyperopt + from freqtrade.optimize.hyperopt_tools import HyperoptTools config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -105,7 +106,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: } # Previous evaluations - epochs = Hyperopt.load_previous_results(results_file) + epochs = HyperoptTools.load_previous_results(results_file) total_epochs = len(epochs) epochs = hyperopt_filter_epochs(epochs, filteroptions) @@ -124,8 +125,8 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: if epochs: val = epochs[n] - Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header, - header_str="Epoch details") + HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, + header_str="Epoch details") def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6b5bc171b..03f34a511 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,36 +4,31 @@ This module contains the hyperopt logic """ -import io import locale import logging import random import warnings -from collections import OrderedDict from datetime import datetime from math import ceil from operator import itemgetter from pathlib import Path -from pprint import pformat from typing import Any, Dict, List, Optional import progressbar -import rapidjson -import tabulate from colorama import Fore, Style from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects -from pandas import DataFrame, isna, json_normalize +from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException -from freqtrade.misc import file_dump_json, plural, round_dict +from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 +from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -169,15 +164,6 @@ class Hyperopt: file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}, log=False) - @staticmethod - def _read_results(results_file: Path) -> List: - """ - Read hyperopt results from file - """ - logger.info("Reading epochs from '%s'", results_file) - data = load(results_file) - return data - def _get_params_details(self, params: Dict) -> Dict: """ Return the params for each space @@ -200,102 +186,16 @@ class Hyperopt: return result - @staticmethod - def print_epoch_details(results, total_epochs: int, print_json: bool, - no_header: bool = False, header_str: str = None) -> None: - """ - Display details of the hyperopt result - """ - params = results.get('params_details', {}) - - # Default header string - if header_str is None: - header_str = "Best result" - - if not no_header: - explanation_str = Hyperopt._format_explanation_string(results, total_epochs) - print(f"\n{header_str}:\n\n{explanation_str}\n") - - if print_json: - result_dict: Dict = {} - for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: - Hyperopt._params_update_for_json(result_dict, params, s) - print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) - - else: - Hyperopt._params_pretty_print(params, 'buy', "Buy hyperspace params:") - Hyperopt._params_pretty_print(params, 'sell', "Sell hyperspace params:") - Hyperopt._params_pretty_print(params, 'roi', "ROI table:") - Hyperopt._params_pretty_print(params, 'stoploss', "Stoploss:") - Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:") - - @staticmethod - def _params_update_for_json(result_dict, params, space: str) -> None: - if space in params: - space_params = Hyperopt._space_params(params, space) - if space in ['buy', 'sell']: - result_dict.setdefault('params', {}).update(space_params) - elif space == 'roi': - # TODO: get rid of OrderedDict when support for python 3.6 will be - # dropped (dicts keep the order as the language feature) - - # Convert keys in min_roi dict to strings because - # rapidjson cannot dump dicts with integer keys... - # OrderedDict is used to keep the numeric order of the items - # in the dict. - result_dict['minimal_roi'] = OrderedDict( - (str(k), v) for k, v in space_params.items() - ) - else: # 'stoploss', 'trailing' - result_dict.update(space_params) - - @staticmethod - def _params_pretty_print(params, space: str, header: str) -> None: - if space in params: - space_params = Hyperopt._space_params(params, space, 5) - params_result = f"\n# {header}\n" - if space == 'stoploss': - params_result += f"stoploss = {space_params.get('stoploss')}" - elif space == 'roi': - # TODO: get rid of OrderedDict when support for python 3.6 will be - # dropped (dicts keep the order as the language feature) - minimal_roi_result = rapidjson.dumps( - OrderedDict( - (str(k), v) for k, v in space_params.items() - ), - default=str, indent=4, number_mode=rapidjson.NM_NATIVE) - params_result += f"minimal_roi = {minimal_roi_result}" - elif space == 'trailing': - - for k, v in space_params.items(): - params_result += f'{k} = {v}\n' - - else: - params_result += f"{space}_params = {pformat(space_params, indent=4)}" - params_result = params_result.replace("}", "\n}").replace("{", "{\n ") - - params_result = params_result.replace("\n", "\n ") - print(params_result) - - @staticmethod - def _space_params(params, space: str, r: int = None) -> Dict: - d = params[space] - # Round floats to `r` digits after the decimal point if requested - return round_dict(d, r) if r else d - - @staticmethod - def is_best_loss(results, current_best_loss: float) -> bool: - return results['loss'] < current_best_loss - def print_results(self, results) -> None: """ Log results if it is better than any previous evaluation + TODO: this should be moved to HyperoptTools too """ is_best = results['is_best'] if self.print_all or is_best: print( - self.get_result_table( + HyperoptTools.get_result_table( self.config, results, self.total_epochs, self.print_all, self.print_colorized, self.hyperopt_table_header @@ -303,166 +203,6 @@ class Hyperopt: ) self.hyperopt_table_header = 2 - @staticmethod - def _format_explanation_string(results, total_epochs) -> str: - return (("*" if results['is_initial_point'] else " ") + - f"{results['current_epoch']:5d}/{total_epochs}: " + - f"{results['results_explanation']} " + - f"Objective: {results['loss']:.5f}") - - @staticmethod - def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, - print_colorized: bool, remove_header: int) -> str: - """ - Log result table - """ - if not results: - return '' - - tabulate.PRESERVE_WHITESPACE = True - - trials = json_normalize(results, max_level=1) - trials['Best'] = '' - if 'results_metrics.winsdrawslosses' not in trials.columns: - # Ensure compatibility with older versions of hyperopt results - trials['results_metrics.winsdrawslosses'] = 'N/A' - - trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.winsdrawslosses', - 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best']] - trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', - 'Total profit', 'Profit', 'Avg duration', 'Objective', - 'is_initial_point', 'is_best'] - trials['is_profit'] = False - trials.loc[trials['is_initial_point'], 'Best'] = '* ' - trials.loc[trials['is_best'], 'Best'] = 'Best' - trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' - trials.loc[trials['Total profit'] > 0, 'is_profit'] = True - trials['Trades'] = trials['Trades'].astype(str) - - trials['Epoch'] = trials['Epoch'].apply( - lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) - ) - trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') - ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') - ) - trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') - ) - - trials['Profit'] = trials.apply( - lambda x: '{:,.8f} {} {}'.format( - x['Total profit'], config['stake_currency'], - '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') - ).rjust(25+len(config['stake_currency'])) - if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), - axis=1 - ) - trials = trials.drop(columns=['Total profit']) - - if print_colorized: - for i in range(len(trials)): - if trials.loc[i]['is_profit']: - for j in range(len(trials.loc[i])-3): - trials.iat[i, j] = "{}{}{}".format(Fore.GREEN, - str(trials.loc[i][j]), Fore.RESET) - if trials.loc[i]['is_best'] and highlight_best: - for j in range(len(trials.loc[i])-3): - trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, - str(trials.loc[i][j]), Style.RESET_ALL) - - trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) - if remove_header > 0: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='orgtbl', - headers='keys', stralign="right" - ) - - table = table.split("\n", remove_header)[remove_header] - elif remove_header < 0: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='psql', - headers='keys', stralign="right" - ) - table = "\n".join(table.split("\n")[0:remove_header]) - else: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='psql', - headers='keys', stralign="right" - ) - return table - - @staticmethod - def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, - csv_file: str) -> None: - """ - Log result to csv-file - """ - if not results: - return - - # Verification for overwrite - if Path(csv_file).is_file(): - logger.error(f"CSV file already exists: {csv_file}") - return - - try: - io.open(csv_file, 'w+').close() - except IOError: - logger.error(f"Failed to create CSV file: {csv_file}") - return - - trials = json_normalize(results, max_level=1) - trials['Best'] = '' - trials['Stake currency'] = config['stake_currency'] - - base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', - 'results_metrics.total_profit', - 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best'] - param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] - trials = trials[base_metrics + param_metrics] - - base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', - 'Stake currency', 'Profit', 'Avg duration', 'Objective', - 'is_initial_point', 'is_best'] - param_columns = list(results[0]['params_dict'].keys()) - trials.columns = base_columns + param_columns - - trials['is_profit'] = False - trials.loc[trials['is_initial_point'], 'Best'] = '*' - trials.loc[trials['is_best'], 'Best'] = 'Best' - trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' - trials.loc[trials['Total profit'] > 0, 'is_profit'] = True - trials['Epoch'] = trials['Epoch'].astype(str) - trials['Trades'] = trials['Trades'].astype(str) - - trials['Total profit'] = trials['Total profit'].apply( - lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" - ) - trials['Profit'] = trials['Profit'].apply( - lambda x: '{:,.2f}'.format(x) if not isna(x) else "" - ) - trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" - ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" - ) - trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x) if x != 100000 else "" - ) - - trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) - trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8') - logger.info(f"CSV file created: {csv_file}") - def has_space(self, space: str) -> bool: """ Tell if the space value is contained in the configuration @@ -626,22 +366,6 @@ class Hyperopt: return parallel(delayed( wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked) - @staticmethod - def load_previous_results(results_file: Path) -> List: - """ - Load data for epochs from the file if we have one - """ - epochs: List = [] - if results_file.is_file() and results_file.stat().st_size > 0: - epochs = Hyperopt._read_results(results_file) - # Detection of some old format, without 'is_best' field saved - if epochs[0].get('is_best') is None: - raise OperationalException( - "The file with Hyperopt results is incompatible with this version " - "of Freqtrade and cannot be loaded.") - logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") - return epochs - def _set_random_state(self, random_state: Optional[int]) -> int: return random_state or random.randint(1, 2**16 - 1) @@ -734,7 +458,7 @@ class Hyperopt: logger.debug(f"Optimizer epoch evaluated: {val}") - is_best = self.is_best_loss(val, self.current_best_loss) + is_best = HyperoptTools.is_best_loss(val, self.current_best_loss) # This value is assigned here and not in the optimization method # to keep proper order in the list of results. That's because # evaluations can take different time. Here they are aligned in the @@ -762,7 +486,7 @@ class Hyperopt: if self.epochs: sorted_epochs = sorted(self.epochs, key=itemgetter('loss')) best_epoch = sorted_epochs[0] - self.print_epoch_details(best_epoch, self.total_epochs, self.print_json) + HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json) else: # This is printed when Ctrl+C is pressed quickly, before first epochs have # a chance to be evaluated. diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py new file mode 100644 index 000000000..d4c347f80 --- /dev/null +++ b/freqtrade/optimize/hyperopt_tools.py @@ -0,0 +1,294 @@ + +import io +import logging +from collections import OrderedDict +from pathlib import Path +from pprint import pformat +from typing import Dict, List + +import rapidjson +import tabulate +from colorama import Fore, Style +from joblib import load +from pandas import isna, json_normalize + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_dict + + +logger = logging.getLogger(__name__) + + +class HyperoptTools(): + + @staticmethod + def _read_results(results_file: Path) -> List: + """ + Read hyperopt results from file + """ + logger.info("Reading epochs from '%s'", results_file) + data = load(results_file) + return data + + @staticmethod + def load_previous_results(results_file: Path) -> List: + """ + Load data for epochs from the file if we have one + """ + epochs: List = [] + if results_file.is_file() and results_file.stat().st_size > 0: + epochs = HyperoptTools._read_results(results_file) + # Detection of some old format, without 'is_best' field saved + if epochs[0].get('is_best') is None: + raise OperationalException( + "The file with HyperoptTools results is incompatible with this version " + "of Freqtrade and cannot be loaded.") + logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") + return epochs + + @staticmethod + def print_epoch_details(results, total_epochs: int, print_json: bool, + no_header: bool = False, header_str: str = None) -> None: + """ + Display details of the hyperopt result + """ + params = results.get('params_details', {}) + + # Default header string + if header_str is None: + header_str = "Best result" + + if not no_header: + explanation_str = HyperoptTools._format_explanation_string(results, total_epochs) + print(f"\n{header_str}:\n\n{explanation_str}\n") + + if print_json: + result_dict: Dict = {} + for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: + HyperoptTools._params_update_for_json(result_dict, params, s) + print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) + + else: + HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:") + HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:") + HyperoptTools._params_pretty_print(params, 'roi', "ROI table:") + HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") + HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") + + @staticmethod + def _params_update_for_json(result_dict, params, space: str) -> None: + if space in params: + space_params = HyperoptTools._space_params(params, space) + if space in ['buy', 'sell']: + result_dict.setdefault('params', {}).update(space_params) + elif space == 'roi': + # TODO: get rid of OrderedDict when support for python 3.6 will be + # dropped (dicts keep the order as the language feature) + + # Convert keys in min_roi dict to strings because + # rapidjson cannot dump dicts with integer keys... + # OrderedDict is used to keep the numeric order of the items + # in the dict. + result_dict['minimal_roi'] = OrderedDict( + (str(k), v) for k, v in space_params.items() + ) + else: # 'stoploss', 'trailing' + result_dict.update(space_params) + + @staticmethod + def _params_pretty_print(params, space: str, header: str) -> None: + if space in params: + space_params = HyperoptTools._space_params(params, space, 5) + params_result = f"\n# {header}\n" + if space == 'stoploss': + params_result += f"stoploss = {space_params.get('stoploss')}" + elif space == 'roi': + # TODO: get rid of OrderedDict when support for python 3.6 will be + # dropped (dicts keep the order as the language feature) + minimal_roi_result = rapidjson.dumps( + OrderedDict( + (str(k), v) for k, v in space_params.items() + ), + default=str, indent=4, number_mode=rapidjson.NM_NATIVE) + params_result += f"minimal_roi = {minimal_roi_result}" + elif space == 'trailing': + + for k, v in space_params.items(): + params_result += f'{k} = {v}\n' + + else: + params_result += f"{space}_params = {pformat(space_params, indent=4)}" + params_result = params_result.replace("}", "\n}").replace("{", "{\n ") + + params_result = params_result.replace("\n", "\n ") + print(params_result) + + @staticmethod + def _space_params(params, space: str, r: int = None) -> Dict: + d = params[space] + # Round floats to `r` digits after the decimal point if requested + return round_dict(d, r) if r else d + + @staticmethod + def is_best_loss(results, current_best_loss: float) -> bool: + return results['loss'] < current_best_loss + + @staticmethod + def _format_explanation_string(results, total_epochs) -> str: + return (("*" if results['is_initial_point'] else " ") + + f"{results['current_epoch']:5d}/{total_epochs}: " + + f"{results['results_explanation']} " + + f"Objective: {results['loss']:.5f}") + + @staticmethod + def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, + print_colorized: bool, remove_header: int) -> str: + """ + Log result table + """ + if not results: + return '' + + tabulate.PRESERVE_WHITESPACE = True + + trials = json_normalize(results, max_level=1) + trials['Best'] = '' + if 'results_metrics.winsdrawslosses' not in trials.columns: + # Ensure compatibility with older versions of hyperopt results + trials['results_metrics.winsdrawslosses'] = 'N/A' + + trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.winsdrawslosses', + 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best']] + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', + 'Total profit', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] + trials['is_profit'] = False + trials.loc[trials['is_initial_point'], 'Best'] = '* ' + trials.loc[trials['is_best'], 'Best'] = 'Best' + trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' + trials.loc[trials['Total profit'] > 0, 'is_profit'] = True + trials['Trades'] = trials['Trades'].astype(str) + + trials['Epoch'] = trials['Epoch'].apply( + lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) + ) + trials['Avg profit'] = trials['Avg profit'].apply( + lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + ) + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + ) + trials['Objective'] = trials['Objective'].apply( + lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') + ) + + trials['Profit'] = trials.apply( + lambda x: '{:,.8f} {} {}'.format( + x['Total profit'], config['stake_currency'], + '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') + ).rjust(25+len(config['stake_currency'])) + if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), + axis=1 + ) + trials = trials.drop(columns=['Total profit']) + + if print_colorized: + for i in range(len(trials)): + if trials.loc[i]['is_profit']: + for j in range(len(trials.loc[i])-3): + trials.iat[i, j] = "{}{}{}".format(Fore.GREEN, + str(trials.loc[i][j]), Fore.RESET) + if trials.loc[i]['is_best'] and highlight_best: + for j in range(len(trials.loc[i])-3): + trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, + str(trials.loc[i][j]), Style.RESET_ALL) + + trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) + if remove_header > 0: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='orgtbl', + headers='keys', stralign="right" + ) + + table = table.split("\n", remove_header)[remove_header] + elif remove_header < 0: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='psql', + headers='keys', stralign="right" + ) + table = "\n".join(table.split("\n")[0:remove_header]) + else: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='psql', + headers='keys', stralign="right" + ) + return table + + @staticmethod + def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, + csv_file: str) -> None: + """ + Log result to csv-file + """ + if not results: + return + + # Verification for overwrite + if Path(csv_file).is_file(): + logger.error(f"CSV file already exists: {csv_file}") + return + + try: + io.open(csv_file, 'w+').close() + except IOError: + logger.error(f"Failed to create CSV file: {csv_file}") + return + + trials = json_normalize(results, max_level=1) + trials['Best'] = '' + trials['Stake currency'] = config['stake_currency'] + + base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.avg_profit', 'results_metrics.median_profit', + 'results_metrics.total_profit', + 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best'] + param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] + trials = trials[base_metrics + param_metrics] + + base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', + 'Stake currency', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] + param_columns = list(results[0]['params_dict'].keys()) + trials.columns = base_columns + param_columns + + trials['is_profit'] = False + trials.loc[trials['is_initial_point'], 'Best'] = '*' + trials.loc[trials['is_best'], 'Best'] = 'Best' + trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' + trials.loc[trials['Total profit'] > 0, 'is_profit'] = True + trials['Epoch'] = trials['Epoch'].astype(str) + trials['Trades'] = trials['Trades'].astype(str) + + trials['Total profit'] = trials['Total profit'].apply( + lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" + ) + trials['Profit'] = trials['Profit'].apply( + lambda x: '{:,.2f}'.format(x) if not isna(x) else "" + ) + trials['Avg profit'] = trials['Avg profit'].apply( + lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" + ) + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" + ) + trials['Objective'] = trials['Objective'].apply( + lambda x: '{:,.5f}'.format(x) if x != 100000 else "" + ) + + trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) + trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8') + logger.info(f"CSV file created: {csv_file}") diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 27875ac94..e21ef4dd1 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -920,7 +920,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): mocker.patch( - 'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', MagicMock(return_value=hyperopt_results) ) @@ -1152,7 +1152,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): def test_hyperopt_show(mocker, capsys, hyperopt_results): mocker.patch( - 'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', MagicMock(return_value=hyperopt_results) ) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 9ebdad2b5..193d997db 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -16,6 +16,7 @@ from freqtrade.commands.optimize_commands import setup_optimize_configuration, s from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, @@ -336,9 +337,9 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) results_file = testdatadir / 'optimize' / 'ut_results.pickle' - hyperopt_epochs = hyperopt._read_results(results_file) + hyperopt_epochs = HyperoptTools._read_results(results_file) assert log_has(f"Reading epochs from '{results_file}'", caplog) assert hyperopt_epochs == epochs mock_load.assert_called_once() @@ -346,7 +347,7 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) statmock = MagicMock() statmock.st_size = 5 @@ -354,16 +355,16 @@ def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: results_file = testdatadir / 'optimize' / 'ut_results.pickle' - hyperopt_epochs = hyperopt.load_previous_results(results_file) + hyperopt_epochs = HyperoptTools.load_previous_results(results_file) assert hyperopt_epochs == epochs mock_load.assert_called_once() del epochs[0]['is_best'] - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) with pytest.raises(OperationalException): - hyperopt.load_previous_results(results_file) + HyperoptTools.load_previous_results(results_file) def test_roi_table_generation(hyperopt) -> None: @@ -453,7 +454,7 @@ def test_format_results(hyperopt): 'is_initial_point': True, } - result = hyperopt._format_explanation_string(results, 1) + result = HyperoptTools._format_explanation_string(results, 1) assert result.find(' 66.67%') assert result.find('Total profit 1.00000000 BTC') assert result.find('2.0000Σ %') @@ -467,7 +468,7 @@ def test_format_results(hyperopt): df = pd.DataFrame.from_records(trades, columns=labels) results_metrics = hyperopt._calculate_results_metrics(df) results['total_profit'] = results_metrics['total_profit'] - result = hyperopt._format_explanation_string(results, 1) + result = HyperoptTools._format_explanation_string(results, 1) assert result.find('Total profit 1.00000000 EUR') @@ -1076,7 +1077,7 @@ def test_print_epoch_details(capsys): 'is_best': True } - Hyperopt.print_epoch_details(test_result, 5, False, no_header=True) + HyperoptTools.print_epoch_details(test_result, 5, False, no_header=True) captured = capsys.readouterr() assert '# Trailing stop:' in captured.out # re.match(r"Pairs for .*", captured.out) From 983c0ef118e5ee6a63d91478e051477300616fcf Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Thu, 18 Mar 2021 09:47:03 +0900 Subject: [PATCH 0015/1386] update stoploss_from_open examples to use helper function --- docs/strategy-advanced.md | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index cda988acd..ddf845fca 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -181,17 +181,9 @@ class AwesomeStrategy(IStrategy): #### Calculating stoploss relative to open price -Stoploss values returned from `custom_stoploss` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. -This can be calculated as: - -``` python -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) - -``` - -For example, say our open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, 0.21)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. +The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. #### Trailing stoploss with positive offset @@ -201,9 +193,7 @@ Use the initial stoploss until the profit is above 4%, then use a trailing stopl ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade - -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) +from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -237,9 +227,7 @@ Instead of continuously trailing behind the current price, this example sets fix ``` python from datetime import datetime from freqtrade.persistence import Trade - -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) +from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -290,7 +278,7 @@ class AwesomeStrategy(IStrategy): # using current_time directly (like below) will only work in backtesting. # so check "runmode" to make sure that it's only used in backtesting/hyperopt if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): - relative_sl = self.custom_info[pair].loc[current_time]['atr] + relative_sl = self.custom_info[pair].loc[current_time]['atr'] # in live / dry-run, it'll be really the current time else: # but we can just use the last entry from an already analyzed dataframe instead From b6e9e74a8b3eb9c2efcf50350555783da6fe104a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Mar 2021 06:46:08 +0100 Subject: [PATCH 0016/1386] Add link between stoploss_from_open and custom_stop documentation --- docs/strategy-advanced.md | 4 +--- docs/strategy-customization.md | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index ddf845fca..4e8ecb67e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -178,10 +178,9 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` - #### Calculating stoploss relative to open price -Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. @@ -189,7 +188,6 @@ The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_ Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. - ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index bf086bc0a..a00928a67 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -600,9 +600,9 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati ``` python - from freqtrade.strategy import IStrategy, stoploss_from_open from datetime import datetime from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, stoploss_from_open class AwesomeStrategy(IStrategy): @@ -621,6 +621,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati ``` + Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. ## Additional data (Wallets) From bf14796d4ceb372545ab9f71844c82c6f6a97ed2 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Thu, 18 Mar 2021 21:50:54 +0900 Subject: [PATCH 0017/1386] revert "Trailing stoploss with positive offset" example as stoploss_from_open() wasn't adding value --- docs/strategy-advanced.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4e8ecb67e..962b750b5 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -178,20 +178,15 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` -#### Calculating stoploss relative to open price - -Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. - -The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. - #### Trailing stoploss with positive offset Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. +Please note that the stoploss can only increase, values lower than the current stoploss are ignored. + ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade -from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -203,15 +198,21 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: - return 1 # return a value bigger than the inital stoploss to keep using the inital stoploss + return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - # Use a minimum of 2.5% and a maximum of 5% - desired_stop_from_open = max(min(current_profit / 2, 0.05), 0.025) + desired_stoploss = current_profit / 2 - return stoploss_from_open(desired_stop_from_open, current_profit) + # Use a minimum of 2.5% and a maximum of 5% + return max(min(desired_stoploss, 0.05), 0.025 ``` +#### Calculating stoploss relative to open price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. + #### Stepped stoploss Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. From dd4d1d82d46341f7ee99b18b5f3eb7237051834e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Mar 2021 14:19:33 +0100 Subject: [PATCH 0018/1386] Update docs/strategy-advanced.md --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 962b750b5..801bc4731 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -204,7 +204,7 @@ class AwesomeStrategy(IStrategy): desired_stoploss = current_profit / 2 # Use a minimum of 2.5% and a maximum of 5% - return max(min(desired_stoploss, 0.05), 0.025 + return max(min(desired_stoploss, 0.05), 0.025) ``` #### Calculating stoploss relative to open price From 4d52732d30b8f55ec683de156c3ee87203398a08 Mon Sep 17 00:00:00 2001 From: Patrick Brunier Date: Thu, 18 Mar 2021 22:38:54 +0100 Subject: [PATCH 0019/1386] Added a small snippet to give users a descent error message, when their start date is afer the stop date. Also updated the tests. --- freqtrade/configuration/timerange.py | 2 ++ tests/test_timerange.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 32bbd02a0..2075b38c6 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -103,5 +103,7 @@ class TimeRange: stop = int(stops) // 1000 else: stop = int(stops) + if start > stop > 0: + raise Exception('Start date is after stop date for timerange "%s"' % text) return TimeRange(stype[0], stype[1], start, stop) raise Exception('Incorrect syntax for timerange "%s"' % text) diff --git a/tests/test_timerange.py b/tests/test_timerange.py index 5c35535f0..cd10e219f 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -30,6 +30,9 @@ def test_parse_timerange_incorrect(): with pytest.raises(Exception, match=r'Incorrect syntax.*'): TimeRange.parse_timerange('-') + with pytest.raises(Exception, match=r'Start date is after stop date for timerange.*'): + TimeRange.parse_timerange('20100523-20100522') + def test_subtract_start(): x = TimeRange('date', 'date', 1274486400, 1438214400) From 0d5833ed9133ff629423143db9c810051f3abf45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Mar 2021 06:40:04 +0100 Subject: [PATCH 0020/1386] Use OperationalException for TimeRange errors --- freqtrade/configuration/timerange.py | 7 +++++-- tests/test_timerange.py | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 2075b38c6..6072e296c 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -7,6 +7,8 @@ from typing import Optional import arrow +from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) @@ -104,6 +106,7 @@ class TimeRange: else: stop = int(stops) if start > stop > 0: - raise Exception('Start date is after stop date for timerange "%s"' % text) + raise OperationalException( + f'Start date is after stop date for timerange "{text}"') return TimeRange(stype[0], stype[1], start, stop) - raise Exception('Incorrect syntax for timerange "%s"' % text) + raise OperationalException(f'Incorrect syntax for timerange "{text}"') diff --git a/tests/test_timerange.py b/tests/test_timerange.py index cd10e219f..dcdaad09d 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -3,6 +3,7 @@ import arrow import pytest from freqtrade.configuration import TimeRange +from freqtrade.exceptions import OperationalException def test_parse_timerange_incorrect(): @@ -27,10 +28,11 @@ def test_parse_timerange_incorrect(): timerange = TimeRange.parse_timerange('-1231006505000') assert TimeRange(None, 'date', 0, 1231006505) == timerange - with pytest.raises(Exception, match=r'Incorrect syntax.*'): + with pytest.raises(OperationalException, match=r'Incorrect syntax.*'): TimeRange.parse_timerange('-') - with pytest.raises(Exception, match=r'Start date is after stop date for timerange.*'): + with pytest.raises(OperationalException, + match=r'Start date is after stop date for timerange.*'): TimeRange.parse_timerange('20100523-20100522') From c1f79922700cd51020cab850905ad6d46599a4f4 Mon Sep 17 00:00:00 2001 From: Maycon Maia Vitali Date: Fri, 19 Mar 2021 10:39:45 -0300 Subject: [PATCH 0021/1386] Added slash to fix a broken formatting On the command table the pipe(|) broke the formatting. --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 833fae1fe..5ecdf8065 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -146,7 +146,7 @@ official commands. You can ask at any moment for help with `/help`. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. -| `/unlock ` | Remove the lock for this pair (or for this lock id). +| `/unlock ` | Remove the lock for this pair (or for this lock id). | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). From fb90901bb3408555331d24e1f46a0144e3afbb55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Mar 2021 20:12:12 +0100 Subject: [PATCH 0022/1386] Fix telegram table for both rendered and github markdown --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 5ecdf8065..377977892 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -146,7 +146,7 @@ official commands. You can ask at any moment for help with `/help`. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. -| `/unlock ` | Remove the lock for this pair (or for this lock id). +| `/unlock ` | Remove the lock for this pair (or for this lock id). | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). From 7ffe1fd36a230712a1f0eb19cc9005bf9564e231 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 07:21:22 +0100 Subject: [PATCH 0023/1386] Fix calculation error for min-trade-stake --- docs/configuration.md | 17 +++++++++++++++++ freqtrade/exchange/exchange.py | 8 ++++---- tests/exchange/test_exchange.py | 18 +++++++++++++----- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index ca1e03b0a..573cbfba2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -156,6 +156,23 @@ Values set in the configuration file always overwrite values set in the strategy There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below. +#### Minimum trade stake + +The minimum stake amount will depend by exchange and pair, and is usually listed in the exchange support pages. +Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.4$. + +The minimum stake amount to buy this pair is therefore `20 * 0.6 ~= 12`. +This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case. + +To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%). + +With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`). + +To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit. + +!!! Warning + Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. + #### Available balance By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fdb34eb41..6b8261afc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -531,16 +531,16 @@ class Exchange: return None # reserve some percent defined in config (5% default) + stoploss - amount_reserve_percent = 1.0 - self._config.get('amount_reserve_percent', + amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += stoploss + amount_reserve_percent += abs(stoploss) # it should not be more than 50% - amount_reserve_percent = max(amount_reserve_percent, 0.5) + amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return max(min_stake_amounts) / amount_reserve_percent + return max(min_stake_amounts) * amount_reserve_percent def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8a8c95a62..942ffd4ab 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,6 +1,7 @@ import copy import logging from datetime import datetime, timedelta, timezone +from math import isclose from random import randint from unittest.mock import MagicMock, Mock, PropertyMock, patch @@ -370,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert result == 2 / 0.9 + assert isclose(result, 2 * 1.1) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -382,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == 2 * 2 / 0.9 + assert isclose(result, 2 * 2 * 1.1) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -394,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == max(2, 2 * 2) / 0.9 + assert isclose(result, max(2, 2 * 2) * 1.1) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -406,7 +407,14 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == max(8, 2 * 2) / 0.9 + assert isclose(result, max(8, 2 * 2) * 1.1) + + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) + assert isclose(result, max(8, 2 * 2) * 1.45) + + # Really big stoploss + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) + assert isclose(result, max(8, 2 * 2) * 1.5) def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -424,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8) def test_set_sandbox(default_conf, mocker): From 69799532a67fc9732e851c71a500e223d4ffb589 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 08:13:10 +0100 Subject: [PATCH 0024/1386] Document usage of open_date_utc closes #4580 --- docs/strategy-advanced.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 801bc4731..7fa824a5b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -146,9 +146,9 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. - if current_time - timedelta(minutes=120) > trade.open_date: + if current_time - timedelta(minutes=120) > trade.open_date_utc: return -0.05 - elif current_time - timedelta(minutes=60) > trade.open_date: + elif current_time - timedelta(minutes=60) > trade.open_date_utc: return -0.10 return 1 ``` @@ -317,7 +317,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to The function must return either `True` (cancel order) or `False` (keep order alive). ``` python -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from freqtrade.persistence import Trade class AwesomeStrategy(IStrategy): @@ -331,21 +331,21 @@ class AwesomeStrategy(IStrategy): } def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5): + if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): return True - elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3): + elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): return True - elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24): + elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): return True return False def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5): + if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): return True - elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3): + elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): return True - elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24): + elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): return True return False ``` From 066dd72210889776c06726ee54bbd1ae798d1f20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 08:34:15 +0100 Subject: [PATCH 0025/1386] add orderbook structure documentation --- docs/strategy-customization.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a00928a67..256b28990 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -436,6 +436,26 @@ if self.dp: dataframe['best_ask'] = ob['asks'][0][0] ``` +The orderbook structure is aligned with the order structure from [ccxt](https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure), so the result will look as follows: + +``` js +{ + 'bids': [ + [ price, amount ], // [ float, float ] + [ price, amount ], + ... + ], + 'asks': [ + [ price, amount ], + [ price, amount ], + //... + ], + //... +} +``` + +Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position. + !!! Warning "Warning about backtesting" The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values. From fe7f3d9c37f70983fdb55459528378969c1f3d71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 11:48:39 +0100 Subject: [PATCH 0026/1386] Add price side validation for market orders --- freqtrade/configuration/config_validation.py | 14 +++++++++ tests/test_configuration.py | 32 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index df9f16f3e..b6029b6a5 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -74,6 +74,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: # validating trailing stoploss _validate_trailing_stoploss(conf) + _validate_price_config(conf) _validate_edge(conf) _validate_whitelist(conf) _validate_protections(conf) @@ -95,6 +96,19 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None: raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.") +def _validate_price_config(conf: Dict[str, Any]) -> None: + """ + When using market orders, price sides must be using the "other" side of the price + """ + if (conf['order_types'].get('buy') == 'market' + and conf['bid_strategy'].get('price_side') != 'ask'): + raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".') + + if (conf['order_types'].get('sell') == 'market' + and conf['ask_strategy'].get('price_side') != 'bid'): + raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".') + + def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if conf.get('stoploss') == 0.0: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 6b3df392b..a0824e65c 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -790,6 +790,38 @@ def test_validate_max_open_trades(default_conf): validate_config_consistency(default_conf) +def test_validate_price_side(default_conf): + default_conf['order_types'] = { + "buy": "limit", + "sell": "limit", + "stoploss": "limit", + "stoploss_on_exchange": False, + } + # Default should pass + validate_config_consistency(default_conf) + + conf = deepcopy(default_conf) + conf['order_types']['buy'] = 'market' + with pytest.raises(OperationalException, + match='Market buy orders require bid_strategy.price_side = "ask".'): + validate_config_consistency(conf) + + conf = deepcopy(default_conf) + conf['order_types']['sell'] = 'market' + with pytest.raises(OperationalException, + match='Market sell orders require ask_strategy.price_side = "bid".'): + validate_config_consistency(conf) + + # Validate inversed case + conf = deepcopy(default_conf) + conf['order_types']['sell'] = 'market' + conf['order_types']['buy'] = 'market' + conf['ask_strategy']['price_side'] = 'bid' + conf['bid_strategy']['price_side'] = 'ask' + + validate_config_consistency(conf) + + def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 with pytest.raises(OperationalException, match='The config stoploss needs to be different ' From 16a54b3616efa47bd394ddb660a00881d1fda989 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 13:08:02 +0100 Subject: [PATCH 0027/1386] Don't require non-mandatory arguments --- freqtrade/configuration/config_validation.py | 8 ++++---- tests/test_freqtradebot.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index b6029b6a5..c7e49f33d 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -100,12 +100,12 @@ def _validate_price_config(conf: Dict[str, Any]) -> None: """ When using market orders, price sides must be using the "other" side of the price """ - if (conf['order_types'].get('buy') == 'market' - and conf['bid_strategy'].get('price_side') != 'ask'): + if (conf.get('order_types', {}).get('buy') == 'market' + and conf.get('bid_strategy', {}).get('price_side') != 'ask'): raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".') - if (conf['order_types'].get('sell') == 'market' - and conf['ask_strategy'].get('price_side') != 'bid'): + if (conf.get('order_types', {}).get('sell') == 'market' + and conf.get('ask_strategy', {}).get('price_side') != 'bid'): raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d7d2e19f6..5ef9960ab 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -94,6 +94,7 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: 'stoploss': 'limit', 'stoploss_on_exchange': True, } + conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) assert freqtrade.strategy.order_types['stoploss_on_exchange'] @@ -128,6 +129,7 @@ def test_order_dict_live(default_conf, mocker, caplog) -> None: 'stoploss': 'limit', 'stoploss_on_exchange': True, } + conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) From 73876b61b491b62f1337b9500eb5e926e53247e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 13:33:49 +0100 Subject: [PATCH 0028/1386] Show potential errors when loading markets --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6b8261afc..5b6e2b20d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -311,8 +311,8 @@ class Exchange: self._markets = self._api.load_markets() self._load_async_markets() self._last_markets_refresh = arrow.utcnow().int_timestamp - except ccxt.BaseError as e: - logger.warning('Unable to initialize markets. Reason: %s', e) + except ccxt.BaseError: + logger.exception('Unable to initialize markets.') def reload_markets(self) -> None: """Reload markets both sync and async if refresh interval has passed """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 942ffd4ab..3439c7a09 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -498,7 +498,7 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) - assert log_has('Unable to initialize markets. Reason: SomeError', caplog) + assert log_has('Unable to initialize markets.', caplog) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() From f4e71c1f145a47c3bd104da5f068b22df902642f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:02:13 +0100 Subject: [PATCH 0029/1386] get_buy_rate tests should be sensible --- tests/test_freqtradebot.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5ef9960ab..8f55a8fe6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -839,17 +839,17 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask ('ask', 4, 5, None, 1, 4), # last not available - uses ask ('ask', 4, 5, None, 0, 4), # last not available - uses ask - ('bid', 10, 20, 10, 0.0, 20), # Full bid side - ('bid', 10, 20, 10, 1.0, 10), # Full last side - ('bid', 10, 20, 10, 0.5, 15), # Between bid and last - ('bid', 10, 20, 10, 0.7, 13), # Between bid and last - ('bid', 10, 20, 10, 0.3, 17), # Between bid and last - ('bid', 4, 5, 10, 1.0, 5), # last bigger than bid - ('bid', 4, 5, 10, 0.5, 5), # last bigger than bid - ('bid', 10, 20, None, 0.5, 20), # last not available - uses bid - ('bid', 4, 5, None, 0.5, 5), # last not available - uses bid - ('bid', 4, 5, None, 1, 5), # last not available - uses bid - ('bid', 4, 5, None, 0, 5), # last not available - uses bid + ('bid', 21, 20, 10, 0.0, 20), # Full bid side + ('bid', 21, 20, 10, 1.0, 10), # Full last side + ('bid', 21, 20, 10, 0.5, 15), # Between bid and last + ('bid', 21, 20, 10, 0.7, 13), # Between bid and last + ('bid', 21, 20, 10, 0.3, 17), # Between bid and last + ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid + ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid + ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid + ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid + ('bid', 6, 5, None, 1, 5), # last not available - uses bid + ('bid', 6, 5, None, 0, 5), # last not available - uses bid ]) def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, last, last_ab, expected) -> None: @@ -858,7 +858,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, default_conf['bid_strategy']['price_side'] = side freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': ask, 'last': last, 'bid': bid})) + return_value={'ask': ask, 'last': last, 'bid': bid}) assert freqtrade.get_buy_rate('ETH/BTC', True) == expected assert not log_has("Using cached buy rate for ETH/BTC.", caplog) From 43d7f9ac67a16b10edb63b5e48a0bc23bc7e5bb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:38:26 +0100 Subject: [PATCH 0030/1386] Add bid_last_balance parameter to interpolate sell prices closes #3270 --- docs/configuration.md | 3 ++- docs/includes/pricing.md | 4 ++++ freqtrade/constants.py | 6 ++++++ freqtrade/freqtradebot.py | 10 ++++++++-- tests/test_freqtradebot.py | 39 ++++++++++++++++++++++++-------------- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 573cbfba2..eb3351b8f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -62,12 +62,13 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). -| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled). +| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer | `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
**Datatype:** Boolean | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
**Datatype:** Float (as ratio) | `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).
*Defaults to `ask`.*
**Datatype:** String (either `ask` or `bid`). +| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled). | `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled).
**Datatype:** Boolean | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md index d8a72cc58..bdf27eb20 100644 --- a/docs/includes/pricing.md +++ b/docs/includes/pricing.md @@ -103,6 +103,10 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. +When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. + +The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price. + ### Market order pricing When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f25f6653d..3a2ed98e9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -165,6 +165,12 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'}, + 'bid_last_balance': { + 'type': 'number', + 'minimum': 0, + 'maximum': 1, + 'exclusiveMaximum': False, + }, 'use_order_book': {'type': 'boolean'}, 'order_book_min': {'type': 'integer', 'minimum': 1}, 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c60d65f72..73f4c91be 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -432,7 +432,7 @@ class FreqtradeBot(LoggingMixin): ticker = self.exchange.fetch_ticker(pair) ticker_rate = ticker[bid_strategy['price_side']] if ticker['last'] and ticker_rate > ticker['last']: - balance = self.config['bid_strategy']['ask_last_balance'] + balance = bid_strategy['ask_last_balance'] ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) used_rate = ticker_rate @@ -745,7 +745,13 @@ class FreqtradeBot(LoggingMixin): logger.warning("Sell Price at location from orderbook could not be determined.") raise PricingError from e else: - rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']] + ticker = self.exchange.fetch_ticker(pair) + ticker_rate = ticker[ask_strategy['price_side']] + if ticker['last'] and ticker_rate < ticker['last']: + balance = ask_strategy.get('bid_last_balance', 0.0) + ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last']) + rate = ticker_rate + if rate is None: raise PricingError(f"Sell-Rate for {pair} was empty.") self._sell_rate_cache[pair] = rate diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8f55a8fe6..c1a17164f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4112,22 +4112,33 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog) -@pytest.mark.parametrize('side,ask,bid,expected', [ - ('bid', 10.0, 11.0, 11.0), - ('bid', 10.0, 11.2, 11.2), - ('bid', 10.0, 11.0, 11.0), - ('bid', 9.8, 11.0, 11.0), - ('bid', 0.0001, 0.002, 0.002), - ('ask', 10.0, 11.0, 10.0), - ('ask', 10.11, 11.2, 10.11), - ('ask', 0.001, 0.002, 0.001), - ('ask', 0.006, 1.0, 0.006), +@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [ + ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side + ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side + ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat + ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid + ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid + ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid + ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), + ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side + ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side + ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat + ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask + ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask + ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask + ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), + ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), + ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), + ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), ]) -def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, expected) -> None: +def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, + last, last_ab, expected) -> None: caplog.set_level(logging.DEBUG) default_conf['ask_strategy']['price_side'] = side - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'bid': bid}) + default_conf['ask_strategy']['bid_last_balance'] = last_ab + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': ask, 'bid': bid, 'last': last}) pair = "ETH/BTC" # Test regular mode @@ -4186,7 +4197,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog): default_conf['ask_strategy']['price_side'] = 'ask' pair = "ETH/BTC" mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': None, 'bid': 0.12}) + return_value={'ask': None, 'bid': 0.12, 'last': None}) ft = get_patched_freqtradebot(mocker, default_conf) with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): ft.get_sell_rate(pair, True) @@ -4195,7 +4206,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog): assert ft.get_sell_rate(pair, True) == 0.12 # Reverse sides mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': 0.13, 'bid': None}) + return_value={'ask': 0.13, 'bid': None, 'last': None}) with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): ft.get_sell_rate(pair, True) From e315a6a0da4e9e0232f3463f12cff7561a64c32d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:58:51 +0100 Subject: [PATCH 0031/1386] assume "last" can miss from a ticker response closes #4573 --- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index 6558f196f..a0579b196 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -64,7 +64,7 @@ class PriceFilter(IPairList): :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ - if ticker['last'] is None or ticker['last'] == 0: + if ticker.get('last', None) is None or ticker.get('last') == 0: self.log_once(f"Removed {pair} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).", logger.info) From 4f5a1e94a7b94c75999404fc06be0d48c7132666 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:30 +0530 Subject: [PATCH 0032/1386] Add .deepsource.toml Signed-off-by: shubhendra --- .deepsource.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 000000000..7a00ca8d6 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,16 @@ +version = 1 + +test_patterns = ["tests/**/test_*.py"] + +exclude_patterns = [ + "docs/**", + "user_data/**", + "build/helpers/**" +] + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" From 62d99a0b7406136f24d51e6d704fcc18969a3f5b Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:30 +0530 Subject: [PATCH 0033/1386] Remove unnecessary comprehension Signed-off-by: shubhendra --- freqtrade/resolvers/strategy_resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index b1b66e3ae..19bd014f9 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -196,9 +196,9 @@ class StrategyResolver(IResolver): strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) - if any([x == 2 for x in [strategy._populate_fun_len, + if any(x == 2 for x in [strategy._populate_fun_len, strategy._buy_fun_len, - strategy._sell_fun_len]]): + strategy._sell_fun_len]): strategy.INTERFACE_VERSION = 1 return strategy From 537ad059bc3b643eb2d022ddb798ed4d84aa8c78 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:31 +0530 Subject: [PATCH 0034/1386] Remove unnecessary use of comprehension Signed-off-by: shubhendra --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 78f45de0b..465f3d443 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -611,7 +611,7 @@ class LocalTrade(): else: # Not used during backtesting, but might be used by a strategy - sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open] + sel_trades = list(LocalTrade.trades + LocalTrade.trades_open) if pair: sel_trades = [trade for trade in sel_trades if trade.pair == pair] From 6d6ad035d6cf7fcedac7697fd69c8948f2e3fd76 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:32 +0530 Subject: [PATCH 0035/1386] Remove length check in favour of truthiness of the object Signed-off-by: shubhendra --- freqtrade/commands/list_commands.py | 2 +- freqtrade/exchange/exchange.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 9e6076dfb..5f53fc824 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -177,7 +177,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: # human-readable formats. print() - if len(pairs): + if pairs: if args.get('print_list', False): # print data as a list, with human-readable summary print(f"{summary_str}: {', '.join(pairs.keys())}.") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5b6e2b20d..9c868df2b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -958,7 +958,7 @@ class Exchange: while True: t = await self._async_fetch_trades(pair, params={self._trades_pagination_arg: from_id}) - if len(t): + if t: # Skip last id since its the key for the next call trades.extend(t[:-1]) if from_id == t[-1][1] or t[-1][0] > until: @@ -990,7 +990,7 @@ class Exchange: # DEFAULT_TRADES_COLUMNS: 1 -> id while True: t = await self._async_fetch_trades(pair, since=since) - if len(t): + if t: since = t[-1][0] trades.extend(t) # Reached the end of the defined-download period From 910e15b17431a8c7be26fde1346d9ef2fb9e45d1 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:33 +0530 Subject: [PATCH 0036/1386] Remove methods with unnecessary super delegation. Signed-off-by: shubhendra --- freqtrade/plugins/pairlist/PerformanceFilter.py | 5 ----- freqtrade/plugins/protections/cooldown_period.py | 3 --- 2 files changed, 8 deletions(-) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 7d91bb77c..c1355f655 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -15,11 +15,6 @@ logger = logging.getLogger(__name__) class PerformanceFilter(IPairList): - def __init__(self, exchange, pairlistmanager, - config: Dict[str, Any], pairlistconfig: Dict[str, Any], - pairlist_pos: int) -> None: - super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - @property def needstickers(self) -> bool: """ diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index f74f83885..197d74c2e 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -15,9 +15,6 @@ class CooldownPeriod(IProtection): has_global_stop: bool = False has_local_stop: bool = True - def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: - super().__init__(config, protection_config) - def _reason(self) -> str: """ LockReason to use From 45da3a70227cc95ee7610e0bb12dabd348b0086e Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:34 +0530 Subject: [PATCH 0037/1386] Refactor unnecessary `else` / `elif` when `if` block has a `continue` statement Signed-off-by: shubhendra --- freqtrade/configuration/directory_operations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 51310f013..1ce8d1461 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -72,6 +72,5 @@ def copy_sample_files(directory: Path, overwrite: bool = False) -> None: if not overwrite: logger.warning(f"File `{targetfile}` exists already, not deploying sample file.") continue - else: - logger.warning(f"File `{targetfile}` exists already, overwriting.") + logger.warning(f"File `{targetfile}` exists already, overwriting.") shutil.copy(str(sourcedir / source), str(targetfile)) From 4d8183491218783c92bc7230d98d00dffdf9b115 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:34 +0530 Subject: [PATCH 0038/1386] Merge `isinstance` calls Signed-off-by: shubhendra --- freqtrade/exchange/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index c66db860f..be0a1e483 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -140,7 +140,7 @@ def retrier(_func=None, retries=API_RETRY_COUNT): logger.warning('retrying %s() still for %s times', f.__name__, count) count -= 1 kwargs.update({'count': count}) - if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + if isinstance(ex, (DDosProtection, RetryableOrderError)): # increasing backoff backoff_delay = calculate_backoff(count + 1, retries) logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") From ac7a1305cbb78d588bbb1d0849370828b17219fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:11 +0000 Subject: [PATCH 0039/1386] Bump ccxt from 1.43.27 to 1.43.89 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.43.27 to 1.43.89. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.43.27...1.43.89) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..7c8841e67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.3 -ccxt==1.43.27 +ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 From 9612ba34ed0c97e97d681c4726c13bc2f5d69048 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:17 +0000 Subject: [PATCH 0040/1386] Bump urllib3 from 1.26.3 to 1.26.4 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.3 to 1.26.4. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.3...1.26.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..0554f326b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 requests==2.25.1 -urllib3==1.26.3 +urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 From 09c7ee9e923b936cd8e236b7253c08ec4d58a309 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:28 +0000 Subject: [PATCH 0041/1386] Bump isort from 5.7.0 to 5.8.0 Bumps [isort](https://github.com/pycqa/isort) from 5.7.0 to 5.8.0. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.7.0...5.8.0) 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 4f0ea7706..02f7fbca8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.14.0 pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-random-order==1.0.4 -isort==5.7.0 +isort==5.8.0 # Convert jupyter notebooks to markdown documents nbconvert==6.0.7 From ea3012e94d78c5119ba3b457a4799f2b714aafee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:35 +0000 Subject: [PATCH 0042/1386] Bump sqlalchemy from 1.3.23 to 1.4.2 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.23 to 1.4.2. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..c714a8f1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.43.27 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 -SQLAlchemy==1.3.23 +SQLAlchemy==1.4.2 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From e39cff522d33e9a529045ec58610e87362a37328 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Mar 2021 17:30:16 +0100 Subject: [PATCH 0043/1386] Remove duplicate dict keys in test --- tests/rpc/test_rpc_apiserver.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 01492b4f2..5a0a04943 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -810,14 +810,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'stoploss_entry_dist_ratio': -0.10448878, 'trade_id': 1, 'close_rate_requested': None, - 'current_rate': 1.099e-05, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, 'fee_open': 0.0025, 'fee_open_cost': None, 'fee_open_currency': None, - 'open_date': ANY, 'is_open': True, 'max_rate': 1.099e-05, 'min_rate': 1.098e-05, From b7702a1e9f121764605144d63c2480a2b82b08cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Mar 2021 19:39:06 +0100 Subject: [PATCH 0044/1386] Improve tests to work with new sqlalchemy version --- tests/test_freqtradebot.py | 2 +- tests/test_persistence.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c1a17164f..486c31090 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2798,7 +2798,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) - sellmock = MagicMock() + sellmock = MagicMock(return_value={'id': '12345555'}) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1820250a5..6a388327c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging from datetime import datetime, timedelta, timezone +from pathlib import Path from types import FunctionType from unittest.mock import MagicMock @@ -21,14 +22,15 @@ def test_init_create_session(default_conf): assert 'scoped_session' in type(Trade.session).__name__ -def test_init_custom_db_url(default_conf, mocker): +def test_init_custom_db_url(default_conf, tmpdir): # Update path to a value other than default, but still in-memory - default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) - create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) + filename = f"{tmpdir}/freqtrade2_test.sqlite" + assert not Path(filename).is_file() + + default_conf.update({'db_url': f'sqlite:///{filename}'}) init_db(default_conf['db_url'], default_conf['dry_run']) - assert create_engine_mock.call_count == 1 - assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' + assert Path(filename).is_file() def test_init_invalid_db_url(default_conf): @@ -49,15 +51,16 @@ def test_init_prod_db(default_conf, mocker): assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' -def test_init_dryrun_db(default_conf, mocker): - default_conf.update({'dry_run': True}) - default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) - - create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) +def test_init_dryrun_db(default_conf, tmpdir): + filename = f"{tmpdir}/freqtrade2_prod.sqlite" + assert not Path(filename).is_file() + default_conf.update({ + 'dry_run': True, + 'db_url': f'sqlite:///{filename}' + }) init_db(default_conf['db_url'], default_conf['dry_run']) - assert create_engine_mock.call_count == 1 - assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' + assert Path(filename).is_file() @pytest.mark.usefixtures("init_persistence") From 4e8999ade3e8e9d39a5e78d175b736ed6d6b7dc1 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Mon, 22 Mar 2021 20:40:11 +0100 Subject: [PATCH 0045/1386] Changed the code for status table a bit so that it splits up the trades per 50 trades, to make sure it can be sent regardless of number of trades Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7ec67e5d0..2d753db70 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -330,8 +330,12 @@ class Telegram(RPCHandler): statlist, head = self._rpc._rpc_status_table( self._config['stake_currency'], self._config.get('fiat_display_currency', '')) - message = tabulate(statlist, headers=head, tablefmt='simple') - self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) + max_trades_per_msg = 50 + for i in range(0, max(int(len(statlist) / max_trades_per_msg), 1)): + message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], + headers=head, + tablefmt='simple') + self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) From 6856963aef37303b40fd29a0745e22ff312dc11c Mon Sep 17 00:00:00 2001 From: rextea Date: Tue, 23 Mar 2021 10:09:41 +0200 Subject: [PATCH 0046/1386] Add confirm_trade_exit and confirm_trade_entry to backtesting --- freqtrade/optimize/backtesting.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0b884dae5..1dcf6428b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,7 +30,6 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) # Indexes for backtest tuples @@ -252,8 +251,17 @@ class Backtesting: sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) - if sell.sell_flag: + time_in_force = self.strategy.order_time_in_force['sell'] + + # confirm_trade_exit + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=False)( + pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=sell_row[LOW_IDX], + time_in_force=time_in_force, + sell_reason=sell.sell_type.value): + return None + + if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) @@ -271,6 +279,15 @@ class Backtesting: except DependencyException: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) + + order_type = self.strategy.order_types['buy'] + time_in_force = self.strategy.order_time_in_force['sell'] + # confirm_trade_entry + if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( + pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], + time_in_force=time_in_force): + return None + if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade trade = LocalTrade( From eb5d69dcd486305d3448caf3eab1152dbd9cf59f Mon Sep 17 00:00:00 2001 From: rextea Date: Tue, 23 Mar 2021 10:12:08 +0200 Subject: [PATCH 0047/1386] Add confirm_trade_exit and confirm_trade_entry to backtesting --- freqtrade/optimize/backtesting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1dcf6428b..080e6b1a2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -253,7 +253,6 @@ class Backtesting: low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) time_in_force = self.strategy.order_time_in_force['sell'] - # confirm_trade_exit if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=False)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=sell_row[LOW_IDX], From dc4ea604dd6d1fc8bd6af7a68e5fba36f3e7c517 Mon Sep 17 00:00:00 2001 From: rextea Date: Tue, 23 Mar 2021 10:19:16 +0200 Subject: [PATCH 0048/1386] Add confirm_trade_exit and confirm_trade_entry to backtesting --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 080e6b1a2..321b60ecd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -254,7 +254,7 @@ class Backtesting: time_in_force = self.strategy.order_time_in_force['sell'] # confirm_trade_exit - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=False)( + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=sell_row[LOW_IDX], time_in_force=time_in_force, sell_reason=sell.sell_type.value): From f51f4b1817efd2451d4877478312f22d9acabefe Mon Sep 17 00:00:00 2001 From: rextea Date: Tue, 23 Mar 2021 10:35:46 +0200 Subject: [PATCH 0049/1386] Add confirm_trade_exit and confirm_trade_entry to backtesting --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 321b60ecd..d858acc1a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,8 @@ class Backtesting: time_in_force = self.strategy.order_time_in_force['sell'] # confirm_trade_exit if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=sell_row[LOW_IDX], + pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, + rate=sell_row[LOW_IDX], time_in_force=time_in_force, sell_reason=sell.sell_type.value): return None From d5301b4d6316f822d387ae6d2947cec5a49e316f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 23 Mar 2021 10:53:09 +0100 Subject: [PATCH 0050/1386] RateLimit should be enabled by default --- config_full.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index 8366774c4..717797933 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -113,7 +113,7 @@ "password": "", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { - "enableRateLimit": false, + "enableRateLimit": true, "rateLimit": 500, "aiohttp_trust_env": false }, From c928cd38dc2e5a67fcf70d7a76d350f5b7549560 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 16:45:42 +0100 Subject: [PATCH 0051/1386] Small bugfix to make sure it shows all the trades Signed-off-by: Erwin Hoeckx --- 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 2d753db70..b83cbf1a2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,7 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 - for i in range(0, max(int(len(statlist) / max_trades_per_msg), 1)): + for i in range(0, max(int(len(statlist) / max_trades_per_msg) + 1, 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head, tablefmt='simple') From 65a9763fa587aa42823956cd4e207f08905e7906 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 16:54:38 +0100 Subject: [PATCH 0052/1386] Fixed an issue when there were exactly 50 trades, it was sending an extra empty table Signed-off-by: Erwin Hoeckx --- 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 b83cbf1a2..61a0188cb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,7 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 - for i in range(0, max(int(len(statlist) / max_trades_per_msg) + 1, 1)): + for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head, tablefmt='simple') From 2fd510e6e4e8a2108f2a64c6ccad72f83fb047d7 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 21:52:46 +0100 Subject: [PATCH 0053/1386] Added comment with an example calculation Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 61a0188cb..2063a4f58 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,6 +331,11 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 + """ + Calculate the number of messages of 50 trades per message + 0.99 is used to make sure that there are no extra (empty) messages + As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message + """ for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head, From d795febf9243e667de70d06a8c2ff30e951f847a Mon Sep 17 00:00:00 2001 From: rextea Date: Wed, 24 Mar 2021 18:26:03 +0200 Subject: [PATCH 0054/1386] Add info to documantation --- docs/bot-basics.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 13694c316..943af0362 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -53,6 +53,7 @@ This loop will be repeated again and again until the bot is stopped. * Calls `bot_loop_start()` once. * Calculate indicators (calls `populate_indicators()` once per pair). * Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair) +* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy) * Loops per candle simulating entry and exit points. * Generate backtest report output From ec15610bff2314e8f47620e410097106878ffd18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Mar 2021 19:21:07 +0100 Subject: [PATCH 0055/1386] Fix isort issue --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d858acc1a..7de5b171c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,6 +30,7 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) # Indexes for backtest tuples From 0ca95aa0c2035b41c9835f2291020222f9ed690d Mon Sep 17 00:00:00 2001 From: rextea Date: Thu, 25 Mar 2021 10:25:25 +0200 Subject: [PATCH 0056/1386] Change rate to acctual close rate --- freqtrade/optimize/backtesting.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7de5b171c..00b2f278d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,7 +30,6 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) # Indexes for backtest tuples @@ -253,20 +252,21 @@ class Backtesting: sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) - time_in_force = self.strategy.order_time_in_force['sell'] - # confirm_trade_exit - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, - rate=sell_row[LOW_IDX], - time_in_force=time_in_force, - sell_reason=sell.sell_type.value): - return None - if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) + + # Confirm trade exit: + time_in_force = self.strategy.order_time_in_force['sell'] + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, + rate=closerate, + time_in_force=time_in_force, + sell_reason=sell.sell_type.value): + return None + trade.close(closerate, show_msg=False) return trade @@ -283,7 +283,7 @@ class Backtesting: order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['sell'] - # confirm_trade_entry + # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], time_in_force=time_in_force): From 292ea8c1d03b0ecf100d9a1935f0718f4c316cae Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Mar 2021 09:34:33 +0100 Subject: [PATCH 0057/1386] Update backtesting.py --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 00b2f278d..765e2844a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,6 +30,7 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) # Indexes for backtest tuples From 0a205f52b06ad9b12aa13d03794e9e3dfd780440 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 23 Mar 2021 10:02:32 +0200 Subject: [PATCH 0058/1386] Optional support for defining hyperopt parameters in a strategy file and reusing common hyperopt/strategy parts. --- freqtrade/optimize/hyperopt.py | 8 ++- freqtrade/optimize/hyperopt_auto.py | 83 +++++++++++++++++++++++++ freqtrade/strategy/__init__.py | 1 + freqtrade/strategy/hyper.py | 93 +++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_auto.py create mode 100644 freqtrade/strategy/hyper.py diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03f34a511..8dd8f01ac 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -26,6 +26,7 @@ from freqtrade.data.history import get_timerange from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules +from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -67,8 +68,11 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) - self.custom_hyperopt.__class__.strategy = self.backtesting.strategy + if self.config['hyperopt'] == 'HyperOptAuto': + self.custom_hyperopt = HyperOptAuto(self.config) + else: + self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) + self.custom_hyperopt.strategy = self.backtesting.strategy self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py new file mode 100644 index 000000000..788bd5a79 --- /dev/null +++ b/freqtrade/optimize/hyperopt_auto.py @@ -0,0 +1,83 @@ +""" +HyperOptAuto class. +This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement +IHyperStrategy interface. +""" +from typing import Any, Callable, Dict, List +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real # noqa + +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +# noinspection PyUnresolvedReferences +class HyperOptAuto(IHyperOpt): + """ + This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. Most of the time + Strategy.HyperOpt class would only implement indicator_space and sell_indicator_space methods, but other hyperopt + methods can be overridden as well. + """ + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + def populate_buy_trend(dataframe: DataFrame, metadata: dict): + for attr_name, attr in self.strategy.enumerate_parameters('buy'): + attr.value = params[attr_name] + return self.strategy.populate_buy_trend(dataframe, metadata) + return populate_buy_trend + + def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + def populate_buy_trend(dataframe: DataFrame, metadata: dict): + for attr_name, attr in self.strategy.enumerate_parameters('sell'): + attr.value = params[attr_name] + return self.strategy.populate_sell_trend(dataframe, metadata) + return populate_buy_trend + + def _get_func(self, name) -> Callable: + """ + Return a function defined in Strategy.HyperOpt class, or one defined in super() class. + :param name: function name. + :return: a requested function. + """ + hyperopt_cls = getattr(self.strategy, 'HyperOpt') + default_func = getattr(super(), name) + if hyperopt_cls: + return getattr(hyperopt_cls, name, default_func) + else: + return default_func + + def _generate_indicator_space(self, category): + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + for attr_name, attr in self.strategy.enumerate_parameters(category): + yield attr.get_space(attr_name) + + def _get_indicator_space(self, category, fallback_method_name): + indicator_space = list(self._generate_indicator_space(category)) + if len(indicator_space) > 0: + return indicator_space + else: + return self._get_func(fallback_method_name)() + + def indicator_space(self) -> List[Dimension]: + return self._get_indicator_space('buy', 'indicator_space') + + def sell_indicator_space(self) -> List[Dimension]: + return self._get_indicator_space('sell', 'sell_indicator_space') + + def generate_roi_table(self, params: Dict) -> Dict[int, float]: + return self._get_func('generate_roi_table')(params) + + def roi_space(self) -> List[Dimension]: + return self._get_func('roi_space')() + + def stoploss_space(self) -> List[Dimension]: + return self._get_func('stoploss_space')() + + def generate_trailing_params(self, params: Dict) -> Dict: + return self._get_func('generate_trailing_params')(params) + + def trailing_space(self) -> List[Dimension]: + return self._get_func('trailing_space')() diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 85148b6ea..80a7c00de 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,4 +2,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy.hyper import IHyperStrategy, Parameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py new file mode 100644 index 000000000..41bb836d4 --- /dev/null +++ b/freqtrade/strategy/hyper.py @@ -0,0 +1,93 @@ +""" +IHyperStrategy interface, hyperoptable Parameter class. +This module defines a base class for auto-hyperoptable strategies. +""" +from abc import ABC +from typing import Union, List, Iterator, Tuple + +from skopt.space import Integer, Real, Categorical + +from freqtrade.strategy.interface import IStrategy + + +class Parameter(object): + """ + Defines a parameter that can be optimized by hyperopt. + """ + default: Union[int, float, str, bool] + space: List[Union[int, float, str, bool]] + category: str + + def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, + category: str = None, **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space. [min, max] for ints and floats or a list of strings for categorial parameters. + :param default: A default value. Required for ints and floats, optional for categorial parameters (first item + from the space will be used). Type of default value determines skopt space used for optimization. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). + """ + assert 'name' not in kwargs, 'Name is determined by parameter field name and can not be specified manually.' + self.value = default + self.space = space + self.category = category + self._space_params = kwargs + if default is None: + assert len(space) > 0 + self.value = space[0] + + def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]: + """ + Create skopt optimization space. + :param name: A name of parameter field. + :return: skopt space of this parameter, or None if parameter is not optimizable (i.e. space is set to None) + """ + if not self.space: + return None + if isinstance(self.value, int): + assert len(self.space) == 2 + return Integer(*self.space, name=name, **self._space_params) + if isinstance(self.value, float): + assert len(self.space) == 2 + return Real(*self.space, name=name, **self._space_params) + + assert len(self.space) > 0 + return Categorical(self.space, name=name, **self._space_params) + + +class IHyperStrategy(IStrategy, ABC): + """ + A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic. + """ + + def __init__(self, config): + super().__init__(config) + self._load_params(getattr(self, 'buy_params', None)) + self._load_params(getattr(self, 'sell_params', None)) + + def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Parameter]]: + """ + Find all optimizeable parameters and return (name, attr) iterator. + :param category: + :return: + """ + assert category in ('buy', 'sell', None) + for attr_name in dir(self): + if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. + attr = getattr(self, attr_name) + if isinstance(attr, Parameter): + if category is None or category == attr.category or attr_name.startswith(category + '_'): + yield attr_name, attr + + def _load_params(self, params: dict) -> None: + """ + Set optimizeable parameter values. + :param params: Dictionary with new parameter values. + """ + if not params: + return + for attr_name, attr in self.enumerate_parameters(): + if attr_name in params: + attr.value = params[attr_name] From bb89e44e19825dc9b9c1bca393ced8a646800cf5 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 10:32:34 +0200 Subject: [PATCH 0059/1386] [SQUASH] Address PR comments. * Split Parameter into IntParameter/FloatParameter/CategoricalParameter. * Rename IHyperStrategy to HyperStrategyMixin and use it as mixin. * --hyperopt parameter is now optional if strategy uses HyperStrategyMixin. * Use OperationalException() instead of asserts. --- freqtrade/commands/cli_options.py | 1 + freqtrade/optimize/hyperopt.py | 7 +- freqtrade/optimize/hyperopt_auto.py | 22 ++-- freqtrade/optimize/hyperopt_interface.py | 7 +- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/hyper.py | 149 +++++++++++++++++------ 6 files changed, 138 insertions(+), 51 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 15c13cec9..8e9f0c994 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -195,6 +195,7 @@ AVAILABLE_CLI_OPTIONS = { '--hyperopt', help='Specify hyperopt class name which will be used by the bot.', metavar='NAME', + required=False, ), "hyperopt_path": Arg( '--hyperopt-path', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8dd8f01ac..b5ee1da1a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,6 +23,7 @@ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange +from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules @@ -68,7 +69,11 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - if self.config['hyperopt'] == 'HyperOptAuto': + if not self.config.get('hyperopt'): + if not getattr(self.backtesting.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy is not auto-hyperoptable. Specify --hyperopt ' + 'parameter or add HyperStrategyMixin mixin to your ' + 'strategy class.') self.custom_hyperopt = HyperOptAuto(self.config) else: self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 788bd5a79..fdc4da14c 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -1,7 +1,7 @@ """ HyperOptAuto class. -This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement -IHyperStrategy interface. +This module implements a convenience auto-hyperopt class, which can be used together with strategies + that implement IHyperStrategy interface. """ from typing import Any, Callable, Dict, List from pandas import DataFrame @@ -13,26 +13,31 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noinspection PyUnresolvedReferences class HyperOptAuto(IHyperOpt): """ - This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. Most of the time - Strategy.HyperOpt class would only implement indicator_space and sell_indicator_space methods, but other hyperopt - methods can be overridden as well. + This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. + Most of the time Strategy.HyperOpt class would only implement indicator_space and + sell_indicator_space methods, but other hyperopt methods can be overridden as well. """ + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) + return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) + return populate_buy_trend def _get_func(self, name) -> Callable: @@ -49,7 +54,8 @@ class HyperOptAuto(IHyperOpt): return default_func def _generate_indicator_space(self, category): - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') for attr_name, attr in self.strategy.enumerate_parameters(category): yield attr.get_space(attr_name) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..2dd8500a6 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,15 +5,14 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Union from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict -from freqtrade.strategy import IStrategy - +from freqtrade.strategy import IStrategy, HyperStrategyMixin logger = logging.getLogger(__name__) @@ -35,7 +34,7 @@ class IHyperOpt(ABC): """ ticker_interval: str # DEPRECATED timeframe: str - strategy: IStrategy + strategy: Union[IStrategy, HyperStrategyMixin] def __init__(self, config: dict) -> None: self.config = config diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 80a7c00de..e395be106 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,5 +2,6 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import IHyperStrategy, Parameter +from freqtrade.strategy.hyper import HyperStrategyMixin, IntParameter, FloatParameter,\ + CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 41bb836d4..8de986950 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,83 +2,158 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ -from abc import ABC -from typing import Union, List, Iterator, Tuple +from typing import Iterator, Tuple, Any, Optional, Sequence from skopt.space import Integer, Real, Categorical -from freqtrade.strategy.interface import IStrategy +from freqtrade.exceptions import OperationalException -class Parameter(object): +class BaseParameter(object): """ Defines a parameter that can be optimized by hyperopt. """ - default: Union[int, float, str, bool] - space: List[Union[int, float, str, bool]] - category: str + category: Optional[str] + default: Any + value: Any + space: Sequence[Any] - def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, - category: str = None, **kwargs): + def __init__(self, *, space: Sequence[Any], default: Any, category: Optional[str] = None, + **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space. [min, max] for ints and floats or a list of strings for categorial parameters. - :param default: A default value. Required for ints and floats, optional for categorial parameters (first item - from the space will be used). Type of default value determines skopt space used for optimization. :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ - assert 'name' not in kwargs, 'Name is determined by parameter field name and can not be specified manually.' - self.value = default - self.space = space + if 'name' in kwargs: + raise OperationalException( + 'Name is determined by parameter field name and can not be specified manually.') self.category = category self._space_params = kwargs - if default is None: - assert len(space) > 0 - self.value = space[0] + self.value = default + self.space = space - def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]: + def __repr__(self): + return f'{self.__class__.__name__}({self.value})' + + +class IntParameter(BaseParameter): + default: int + value: int + space: Sequence[int] + + def __init__(self, *, space: Sequence[int], default: int, category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [min, max]. + :param default: A default value. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Integer. + """ + if len(space) != 2: + raise OperationalException('IntParameter space must be [min, max]') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Integer: """ Create skopt optimization space. :param name: A name of parameter field. - :return: skopt space of this parameter, or None if parameter is not optimizable (i.e. space is set to None) """ - if not self.space: - return None - if isinstance(self.value, int): - assert len(self.space) == 2 - return Integer(*self.space, name=name, **self._space_params) - if isinstance(self.value, float): - assert len(self.space) == 2 - return Real(*self.space, name=name, **self._space_params) + return Integer(*self.space, name=name, **self._space_params) - assert len(self.space) > 0 + +class FloatParameter(BaseParameter): + default: float + value: float + space: Sequence[float] + + def __init__(self, *, space: Sequence[float], default: float, category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [min, max]. + :param default: A default value. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Real. + """ + if len(space) != 2: + raise OperationalException('IntParameter space must be [min, max]') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Real: + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ + return Real(*self.space, name=name, **self._space_params) + + +class CategoricalParameter(BaseParameter): + default: Any + value: Any + space: Sequence[Any] + + def __init__(self, *, space: Sequence[Any], default: Optional[Any] = None, + category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [a, b, ...]. + :param default: A default value. If not specified, first item from specified space will be + used. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Categorical. + """ + if len(space) < 2: + raise OperationalException( + 'IntParameter space must be [a, b, ...] (at least two parameters)') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Categorical: + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ return Categorical(self.space, name=name, **self._space_params) -class IHyperStrategy(IStrategy, ABC): +class HyperStrategyMixin(object): """ - A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic. + A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell + strategy logic. """ - def __init__(self, config): - super().__init__(config) + # Hint that class can be used with HyperOptAuto. + HYPER_STRATEGY = 1 + + def __init__(self): + """ + Initialize hyperoptable strategy mixin. + :param config: + """ self._load_params(getattr(self, 'buy_params', None)) self._load_params(getattr(self, 'sell_params', None)) - def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Parameter]]: + def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: """ Find all optimizeable parameters and return (name, attr) iterator. :param category: :return: """ - assert category in ('buy', 'sell', None) + if category not in ('buy', 'sell', None): + raise OperationalException('Category must be one of: "buy", "sell", None.') for attr_name in dir(self): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) - if isinstance(attr, Parameter): - if category is None or category == attr.category or attr_name.startswith(category + '_'): + if issubclass(attr.__class__, BaseParameter): + if category is None or category == attr.category or \ + attr_name.startswith(category + '_'): yield attr_name, attr def _load_params(self, params: dict) -> None: From 2d13e5fd5052511d1077632c281099977a548573 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 11:17:17 +0200 Subject: [PATCH 0060/1386] [SQUASH] Oopsies. --- freqtrade/optimize/hyperopt_auto.py | 2 +- freqtrade/strategy/hyper.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index fdc4da14c..41f086ad2 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,10 +7,10 @@ from typing import Any, Callable, Dict, List from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa +from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt -# noinspection PyUnresolvedReferences class HyperOptAuto(IHyperOpt): """ This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 8de986950..154dcead6 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,7 +2,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ -from typing import Iterator, Tuple, Any, Optional, Sequence +from typing import Iterator, Tuple, Any, Optional, Sequence, Union from skopt.space import Integer, Real, Categorical @@ -22,7 +22,8 @@ class BaseParameter(object): **kwargs): """ Initialize hyperopt-optimizable parameter. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ @@ -37,6 +38,9 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' + def get_space(self, name: str) -> Union[Integer, Real, Categorical]: + raise NotImplementedError() + class IntParameter(BaseParameter): default: int @@ -49,7 +53,8 @@ class IntParameter(BaseParameter): Initialize hyperopt-optimizable parameter. :param space: Optimization space, [min, max]. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ @@ -76,7 +81,8 @@ class FloatParameter(BaseParameter): Initialize hyperopt-optimizable parameter. :param space: Optimization space, [min, max]. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ From e9f0babe8a7276cdb1fa3f22cb2b21368e209c31 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 16:03:38 +0200 Subject: [PATCH 0061/1386] [SQUASH] Use HyperStrategyMixin as part of IStrategy interface. --- freqtrade/optimize/hyperopt.py | 4 ---- freqtrade/optimize/hyperopt_auto.py | 10 ---------- freqtrade/optimize/hyperopt_interface.py | 6 +++--- freqtrade/strategy/__init__.py | 3 +-- freqtrade/strategy/hyper.py | 4 ---- freqtrade/strategy/interface.py | 3 ++- 6 files changed, 6 insertions(+), 24 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b5ee1da1a..4926bf1b3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -70,10 +70,6 @@ class Hyperopt: self.backtesting = Backtesting(self.config) if not self.config.get('hyperopt'): - if not getattr(self.backtesting.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy is not auto-hyperoptable. Specify --hyperopt ' - 'parameter or add HyperStrategyMixin mixin to your ' - 'strategy class.') self.custom_hyperopt = HyperOptAuto(self.config) else: self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 41f086ad2..753e8175e 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,7 +7,6 @@ from typing import Any, Callable, Dict, List from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa -from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -19,9 +18,6 @@ class HyperOptAuto(IHyperOpt): """ def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): attr.value = params[attr_name] @@ -30,9 +26,6 @@ class HyperOptAuto(IHyperOpt): return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): attr.value = params[attr_name] @@ -54,9 +47,6 @@ class HyperOptAuto(IHyperOpt): return default_func def _generate_indicator_space(self, category): - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - for attr_name, attr in self.strategy.enumerate_parameters(category): yield attr.get_space(attr_name) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 2dd8500a6..46adf55b8 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,14 +5,14 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict -from freqtrade.strategy import IStrategy, HyperStrategyMixin +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ class IHyperOpt(ABC): """ ticker_interval: str # DEPRECATED timeframe: str - strategy: Union[IStrategy, HyperStrategyMixin] + strategy: IStrategy def __init__(self, config: dict) -> None: self.config = config diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index e395be106..a300c601b 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,6 +2,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import HyperStrategyMixin, IntParameter, FloatParameter,\ - CategoricalParameter +from freqtrade.strategy.hyper import IntParameter, FloatParameter, CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 154dcead6..53a0c6462 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -135,13 +135,9 @@ class HyperStrategyMixin(object): strategy logic. """ - # Hint that class can be used with HyperOptAuto. - HYPER_STRATEGY = 1 - def __init__(self): """ Initialize hyperoptable strategy mixin. - :param config: """ self._load_params(getattr(self, 'buy_params', None)) self._load_params(getattr(self, 'sell_params', None)) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6d40e56cc..b00e0ccb8 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade +from freqtrade.strategy.hyper import HyperStrategyMixin from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -59,7 +60,7 @@ class SellCheckTuple(NamedTuple): sell_type: SellType -class IStrategy(ABC): +class IStrategy(ABC, HyperStrategyMixin): """ Interface for freqtrade strategies Defines the mandatory structure must follow any custom strategies From 11689100e7c2b6a8d8fdcb23060d2f97e3aa206f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 16:24:24 +0200 Subject: [PATCH 0062/1386] [SQUASH] Fix exception when HyperOpt nested class is not defined. --- freqtrade/optimize/hyperopt_auto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 753e8175e..08269b092 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -39,7 +39,7 @@ class HyperOptAuto(IHyperOpt): :param name: function name. :return: a requested function. """ - hyperopt_cls = getattr(self.strategy, 'HyperOpt') + hyperopt_cls = getattr(self.strategy, 'HyperOpt', None) default_func = getattr(super(), name) if hyperopt_cls: return getattr(hyperopt_cls, name, default_func) From fd45dfd89413cf938f14e9e65335ce8a21f316f1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 25 Mar 2021 10:00:52 +0200 Subject: [PATCH 0063/1386] [SQUASH] Make skopt imports optional. --- freqtrade/optimize/hyperopt_auto.py | 15 +++++++++------ freqtrade/strategy/hyper.py | 12 +++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 08269b092..fb8adfe6b 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -3,9 +3,12 @@ HyperOptAuto class. This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement IHyperStrategy interface. """ +from contextlib import suppress from typing import Any, Callable, Dict, List + from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa +with suppress(ImportError): + from skopt.space import Dimension from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -57,23 +60,23 @@ class HyperOptAuto(IHyperOpt): else: return self._get_func(fallback_method_name)() - def indicator_space(self) -> List[Dimension]: + def indicator_space(self) -> List['Dimension']: return self._get_indicator_space('buy', 'indicator_space') - def sell_indicator_space(self) -> List[Dimension]: + def sell_indicator_space(self) -> List['Dimension']: return self._get_indicator_space('sell', 'sell_indicator_space') def generate_roi_table(self, params: Dict) -> Dict[int, float]: return self._get_func('generate_roi_table')(params) - def roi_space(self) -> List[Dimension]: + def roi_space(self) -> List['Dimension']: return self._get_func('roi_space')() - def stoploss_space(self) -> List[Dimension]: + def stoploss_space(self) -> List['Dimension']: return self._get_func('stoploss_space')() def generate_trailing_params(self, params: Dict) -> Dict: return self._get_func('generate_trailing_params')(params) - def trailing_space(self) -> List[Dimension]: + def trailing_space(self) -> List['Dimension']: return self._get_func('trailing_space')() diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 53a0c6462..0378be1d5 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,9 +2,11 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ +from contextlib import suppress from typing import Iterator, Tuple, Any, Optional, Sequence, Union -from skopt.space import Integer, Real, Categorical +with suppress(ImportError): + from skopt.space import Integer, Real, Categorical from freqtrade.exceptions import OperationalException @@ -38,7 +40,7 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' - def get_space(self, name: str) -> Union[Integer, Real, Categorical]: + def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: raise NotImplementedError() @@ -62,7 +64,7 @@ class IntParameter(BaseParameter): raise OperationalException('IntParameter space must be [min, max]') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Integer: + def get_space(self, name: str) -> 'Integer': """ Create skopt optimization space. :param name: A name of parameter field. @@ -90,7 +92,7 @@ class FloatParameter(BaseParameter): raise OperationalException('IntParameter space must be [min, max]') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Real: + def get_space(self, name: str) -> 'Real': """ Create skopt optimization space. :param name: A name of parameter field. @@ -121,7 +123,7 @@ class CategoricalParameter(BaseParameter): 'IntParameter space must be [a, b, ...] (at least two parameters)') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Categorical: + def get_space(self, name: str) -> 'Categorical': """ Create skopt optimization space. :param name: A name of parameter field. From 424cd2a91422f0f85d1b4362a38ff7fbb3950798 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 25 Mar 2021 17:58:11 +0200 Subject: [PATCH 0064/1386] [SQUASH] Use "space" instead of category. --- freqtrade/strategy/hyper.py | 73 +++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0378be1d5..9f7fc3fb6 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -18,13 +18,13 @@ class BaseParameter(object): category: Optional[str] default: Any value: Any - space: Sequence[Any] + opt_range: Sequence[Any] - def __init__(self, *, space: Sequence[Any], default: Any, category: Optional[str] = None, + def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). @@ -32,10 +32,10 @@ class BaseParameter(object): if 'name' in kwargs: raise OperationalException( 'Name is determined by parameter field name and can not be specified manually.') - self.category = category + self.category = space self._space_params = kwargs self.value = default - self.space = space + self.opt_range = opt_range def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -47,88 +47,97 @@ class BaseParameter(object): class IntParameter(BaseParameter): default: int value: int - space: Sequence[int] + opt_range: Sequence[int] - def __init__(self, *, space: Sequence[int], default: int, category: Optional[str] = None, - **kwargs): + def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, + space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [min, max]. + :param low: lower end of optimization space or [low, high]. + :param high: high end of optimization space. Must be none of entire range is passed first parameter. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ - if len(space) != 2: - raise OperationalException('IntParameter space must be [min, max]') - super().__init__(space=space, default=default, category=category, **kwargs) + if high is None: + if len(low) != 2: + raise OperationalException('IntParameter space must be [low, high]') + opt_range = low + else: + opt_range = [low, high] + super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Integer': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Integer(*self.space, name=name, **self._space_params) + return Integer(*self.opt_range, name=name, **self._space_params) class FloatParameter(BaseParameter): default: float value: float - space: Sequence[float] + opt_range: Sequence[float] - def __init__(self, *, space: Sequence[float], default: float, category: Optional[str] = None, - **kwargs): + def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, + default: float, space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [min, max]. + :param low: lower end of optimization space or [low, high]. + :param high: high end of optimization space. Must be none of entire range is passed first parameter. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ - if len(space) != 2: - raise OperationalException('IntParameter space must be [min, max]') - super().__init__(space=space, default=default, category=category, **kwargs) + if high is None: + if len(low) != 2: + raise OperationalException('IntParameter space must be [low, high]') + opt_range = low + else: + opt_range = [low, high] + super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Real': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Real(*self.space, name=name, **self._space_params) + return Real(*self.opt_range, name=name, **self._space_params) class CategoricalParameter(BaseParameter): default: Any value: Any - space: Sequence[Any] + opt_range: Sequence[Any] - def __init__(self, *, space: Sequence[Any], default: Optional[Any] = None, - category: Optional[str] = None, - **kwargs): + def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, + space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [a, b, ...]. + :param categories: Optimization space, [a, b, ...]. :param default: A default value. If not specified, first item from specified space will be used. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Categorical. """ - if len(space) < 2: + if len(categories) < 2: raise OperationalException( 'IntParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(space=space, default=default, category=category, **kwargs) + super().__init__(opt_range=categories, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Categorical': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Categorical(self.space, name=name, **self._space_params) + return Categorical(self.opt_range, name=name, **self._space_params) class HyperStrategyMixin(object): From bbe6ece38ddd89ca3c4174e7c9a5d824216a9a98 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 14:25:17 +0200 Subject: [PATCH 0065/1386] [SQUASH] Fix parameter configs not loading. --- freqtrade/strategy/hyper.py | 2 +- freqtrade/strategy/interface.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 9f7fc3fb6..0b3021af2 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -146,7 +146,7 @@ class HyperStrategyMixin(object): strategy logic. """ - def __init__(self): + def __init__(self, *args, **kwargs): """ Initialize hyperoptable strategy mixin. """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b00e0ccb8..54c7f2353 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -141,6 +141,7 @@ class IStrategy(ABC, HyperStrategyMixin): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} + super().__init__(config) @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From 40f5c7853ed06422c53e3e320f670266f0f6cb64 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 14:25:49 +0200 Subject: [PATCH 0066/1386] [SQUASH] Add a way to temporarily disable a parameter (excludes from parameter loading/hyperopt) and print parameter values when executed. --- freqtrade/optimize/hyperopt_auto.py | 3 ++- freqtrade/strategy/hyper.py | 32 +++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index fb8adfe6b..31a11e303 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -51,7 +51,8 @@ class HyperOptAuto(IHyperOpt): def _generate_indicator_space(self, category): for attr_name, attr in self.strategy.enumerate_parameters(category): - yield attr.get_space(attr_name) + if attr.enabled: + yield attr.get_space(attr_name) def _get_indicator_space(self, category, fallback_method_name): indicator_space = list(self._generate_indicator_space(category)) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0b3021af2..e6f63f5a5 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,6 +2,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ +import logging from contextlib import suppress from typing import Iterator, Tuple, Any, Optional, Sequence, Union @@ -11,6 +12,9 @@ with suppress(ImportError): from freqtrade.exceptions import OperationalException +logger = logging.getLogger(__name__) + + class BaseParameter(object): """ Defines a parameter that can be optimized by hyperopt. @@ -21,7 +25,7 @@ class BaseParameter(object): opt_range: Sequence[Any] def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, - **kwargs): + enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if @@ -36,6 +40,7 @@ class BaseParameter(object): self._space_params = kwargs self.value = default self.opt_range = opt_range + self.enabled = enabled def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -50,7 +55,7 @@ class IntParameter(BaseParameter): opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, - space: Optional[str] = None, **kwargs): + space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: lower end of optimization space or [low, high]. @@ -67,7 +72,8 @@ class IntParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Integer': """ @@ -83,7 +89,7 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, - default: float, space: Optional[str] = None, **kwargs): + default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: lower end of optimization space or [low, high]. @@ -100,7 +106,8 @@ class FloatParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Real': """ @@ -116,7 +123,7 @@ class CategoricalParameter(BaseParameter): opt_range: Sequence[Any] def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, - space: Optional[str] = None, **kwargs): + space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param categories: Optimization space, [a, b, ...]. @@ -130,7 +137,8 @@ class CategoricalParameter(BaseParameter): if len(categories) < 2: raise OperationalException( 'IntParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, **kwargs) + super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Categorical': """ @@ -167,7 +175,8 @@ class HyperStrategyMixin(object): if issubclass(attr.__class__, BaseParameter): if category is None or category == attr.category or \ attr_name.startswith(category + '_'): - yield attr_name, attr + if attr.enabled: + yield attr_name, attr def _load_params(self, params: dict) -> None: """ @@ -178,4 +187,9 @@ class HyperStrategyMixin(object): return for attr_name, attr in self.enumerate_parameters(): if attr_name in params: - attr.value = params[attr_name] + if attr.enabled: + attr.value = params[attr_name] + logger.info(f'attr_name = {attr.value}') + else: + logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' + f'Default value "{attr.value}" used.') From e934d3ddfbd32ab5cc897883d96c242eec66c5db Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 16:55:48 +0200 Subject: [PATCH 0067/1386] [SQUASH] Oopsie. --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e6f63f5a5..51f937fca 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -88,7 +88,7 @@ class FloatParameter(BaseParameter): value: float opt_range: Sequence[float] - def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, + def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. From 39bfe5e1a79263b18c10e5b8ca51c58705885e66 Mon Sep 17 00:00:00 2001 From: Masoud Azizi Date: Fri, 26 Mar 2021 22:50:27 +0430 Subject: [PATCH 0068/1386] Thee to the --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 69bc57d1a..96c7354b9 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -165,7 +165,7 @@ Depending on the space you want to optimize, only some of the below are required * fill `sell_indicator_space` - for sell signal optimization !!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. Optional in hyperopt - can also be loaded from a strategy (recommended): From 786ddc6a9114dd8b26978be7bbeb1c67db44f48a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 10:40:48 +0100 Subject: [PATCH 0069/1386] remove unused imports --- freqtrade/optimize/hyperopt.py | 1 - freqtrade/optimize/hyperopt_auto.py | 2 ++ freqtrade/optimize/hyperopt_interface.py | 1 + freqtrade/strategy/__init__.py | 2 +- freqtrade/strategy/hyper.py | 19 ++++++++++--------- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 4926bf1b3..a680d85c8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,6 @@ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 31a11e303..847d8697a 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,6 +7,8 @@ from contextlib import suppress from typing import Any, Callable, Dict, List from pandas import DataFrame + + with suppress(ImportError): from skopt.space import Dimension diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 46adf55b8..561fb8e11 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -14,6 +14,7 @@ from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict from freqtrade.strategy import IStrategy + logger = logging.getLogger(__name__) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index a300c601b..bc0c45f7c 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) +from freqtrade.strategy.hyper import CategoricalParameter, FloatParameter, IntParameter from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import IntParameter, FloatParameter, CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 51f937fca..32b03d57e 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -4,7 +4,8 @@ This module defines a base class for auto-hyperoptable strategies. """ import logging from contextlib import suppress -from typing import Iterator, Tuple, Any, Optional, Sequence, Union +from typing import Any, Iterator, Optional, Sequence, Tuple, Union + with suppress(ImportError): from skopt.space import Integer, Real, Categorical @@ -58,12 +59,12 @@ class IntParameter(BaseParameter): space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param low: lower end of optimization space or [low, high]. - :param high: high end of optimization space. Must be none of entire range is passed first parameter. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none of entire range is passed first parameter. :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ if high is None: @@ -92,12 +93,12 @@ class FloatParameter(BaseParameter): default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param low: lower end of optimization space or [low, high]. - :param high: high end of optimization space. Must be none of entire range is passed first parameter. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none if entire range is passed first parameter. :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ if high is None: From 71e2134694c88815d0c4b39e704abdbfbcadfa30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 11:26:26 +0100 Subject: [PATCH 0070/1386] Add some simple tests for hyperoptParameters --- freqtrade/strategy/hyper.py | 14 +- tests/rpc/test_rpc_apiserver.py | 6 +- .../strategy/strats/hyperoptable_strategy.py | 170 ++++++++++++++++++ tests/strategy/test_interface.py | 38 +++- tests/strategy/test_strategy_loading.py | 6 +- 5 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 tests/strategy/strats/hyperoptable_strategy.py diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 32b03d57e..b8bfef767 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -67,8 +67,10 @@ class IntParameter(BaseParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ - if high is None: - if len(low) != 2: + if high is not None and isinstance(low, Sequence): + raise OperationalException('IntParameter space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException('IntParameter space must be [low, high]') opt_range = low else: @@ -101,9 +103,11 @@ class FloatParameter(BaseParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ - if high is None: - if len(low) != 2: - raise OperationalException('IntParameter space must be [low, high]') + if high is not None and isinstance(low, Sequence): + raise OperationalException('FloatParameter space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException('FloatParameter space must be [low, high]') opt_range = low else: opt_range = [low, high] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5a0a04943..bef70a5dd 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1149,7 +1149,11 @@ def test_api_strategies(botclient): rc = client_get(client, f"{BASE_URI}/strategies") assert_response(rc) - assert rc.json() == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} + assert rc.json() == {'strategies': [ + 'DefaultStrategy', + 'HyperoptableStrategy', + 'TestStrategyLegacy' + ]} def test_api_strategy(botclient): diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py new file mode 100644 index 000000000..8cde28321 --- /dev/null +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -0,0 +1,170 @@ +# 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 FloatParameter, IntParameter, IStrategy + + +class HyperoptableStrategy(IStrategy): + """ + Default Strategy provided by 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 = 2 + + # 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 ticker interval for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': '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 = { + 'buy': 'gtc', + 'sell': 'gtc', + } + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74 + } + + buy_rsi = IntParameter([0, 50], default=30, space='buy') + buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # 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_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + 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) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + 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'] > 0.5) + ), + 'sell'] = 1 + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f158a1518..9c831d194 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 +from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -10,7 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -552,3 +553,38 @@ def test_strategy_safe_wrapper(value): assert type(ret) == type(value) assert ret == value + + +def test_hyperopt_parameters(): + with pytest.raises(OperationalException, match=r"Name is determined.*"): + IntParameter(low=0, high=5, default=1, name='hello') + + with pytest.raises(OperationalException, match=r"IntParameter space must be.*"): + IntParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"FloatParameter space must be.*"): + FloatParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"IntParameter space invalid\."): + IntParameter([0, 10], high=7, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): + FloatParameter([0, 10], high=7, default=5, space='buy') + + x = BaseParameter(opt_range=[0, 1], default=1, space='buy') + with pytest.raises(NotImplementedError): + x.get_space('space') + + fltpar = IntParameter(low=0, high=5, default=1, space='buy') + assert fltpar.value == 1 + + +def test_auto_hyperopt_interface(default_conf): + default_conf.update({'strategy': 'HyperoptableStrategy'}) + PairLocks.timeframe = default_conf['timeframe'] + strategy = StrategyResolver.load_strategy(default_conf) + + assert strategy.buy_rsi.value == strategy.buy_params['buy_rsi'] + # PlusDI is NOT in the buy-params, so default should be used + assert strategy.buy_plusdi.value == 0.5 + assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi'] diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 1c692d2da..965c3d37b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,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) == 2 + assert len(strategies) == 3 assert isinstance(strategies[0], dict) @@ -43,10 +43,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) == 3 + assert len(strategies) == 4 # 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]) == 2 + assert len([x for x in strategies if x['class'] is not None]) == 3 assert len([x for x in strategies if x['class'] is None]) == 1 From 4fd7bedcb28f98a8920e51b41a5b7300e62f85de Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 11:32:51 +0100 Subject: [PATCH 0071/1386] Sort imports ... --- tests/strategy/test_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 9c831d194..5dd459238 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 -from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -14,6 +13,7 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re From 7fb34f7e25e1a9d08b3200fb8a0c7b57df773a52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 11:34:11 +0100 Subject: [PATCH 0072/1386] Version bump 2021.3 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2205d284d..5e2a1f88e 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.2' +__version__ = '2021.3' if __version__ == 'develop': From 8022386404e4aca3fdad77e8c43464990eb11c0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 18:00:07 +0100 Subject: [PATCH 0073/1386] Type custom_hyperopt --- freqtrade/optimize/hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a680d85c8..f7c2da4ec 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -62,6 +62,7 @@ class Hyperopt: hyperopt = Hyperopt(config) hyperopt.start() """ + custom_hyperopt: IHyperOpt def __init__(self, config: Dict[str, Any]) -> None: self.config = config From 20f7e9b4b79dcc9db09592826289d7a030ec5558 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 19:31:54 +0200 Subject: [PATCH 0074/1386] Make BaseParameter get_space abstract --- freqtrade/strategy/hyper.py | 8 ++++++-- tests/strategy/test_interface.py | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index b8bfef767..461e6314f 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -3,6 +3,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ import logging +from abc import ABC, abstractmethod from contextlib import suppress from typing import Any, Iterator, Optional, Sequence, Tuple, Union @@ -16,7 +17,7 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -class BaseParameter(object): +class BaseParameter(ABC): """ Defines a parameter that can be optimized by hyperopt. """ @@ -46,8 +47,11 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' + @abstractmethod def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: - raise NotImplementedError() + """ + Get-space - will be used by Hyperopt to get the hyperopt Space + """ class IntParameter(BaseParameter): diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5dd459238..e9d57dd17 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -571,9 +571,8 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): FloatParameter([0, 10], high=7, default=5, space='buy') - x = BaseParameter(opt_range=[0, 1], default=1, space='buy') - with pytest.raises(NotImplementedError): - x.get_space('space') + with pytest.raises(TypeError): + BaseParameter(opt_range=[0, 1], default=1, space='buy') fltpar = IntParameter(low=0, high=5, default=1, space='buy') assert fltpar.value == 1 From 929f3296079b4536bfb163b544dcf4ffda46f6ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 19:49:20 +0200 Subject: [PATCH 0075/1386] more tests --- freqtrade/strategy/hyper.py | 2 +- .../strategy/strats/hyperoptable_strategy.py | 6 ++++-- tests/strategy/test_interface.py | 21 +++++++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 461e6314f..64e457d75 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -145,7 +145,7 @@ class CategoricalParameter(BaseParameter): """ if len(categories) < 2: raise OperationalException( - 'IntParameter space must be [a, b, ...] (at least two parameters)') + 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, **kwargs) diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 8cde28321..97d2092d4 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -55,12 +55,14 @@ class HyperoptableStrategy(IStrategy): } sell_params = { - 'sell_rsi': 74 + 'sell_rsi': 74, + 'sell_minusdi': 0.4 } buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', enabled=False) def informative_pairs(self): """ @@ -164,7 +166,7 @@ class HyperoptableStrategy(IStrategy): ) | ( (dataframe['adx'] > 70) & - (dataframe['minus_di'] > 0.5) + (dataframe['minus_di'] > self.sell_minusdi.value) ), 'sell'] = 1 return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e9d57dd17..72b78338d 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,7 +13,8 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter +from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, FloatParameter, + IntParameter) from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -556,6 +557,7 @@ def test_strategy_safe_wrapper(value): def test_hyperopt_parameters(): + from skopt.space import Categorical, Integer, Real with pytest.raises(OperationalException, match=r"Name is determined.*"): IntParameter(low=0, high=5, default=1, name='hello') @@ -571,12 +573,24 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): FloatParameter([0, 10], high=7, default=5, space='buy') + with pytest.raises(OperationalException, match=r"CategoricalParameter space must.*"): + CategoricalParameter(['aa'], default='aa', space='buy') + with pytest.raises(TypeError): BaseParameter(opt_range=[0, 1], default=1, space='buy') - fltpar = IntParameter(low=0, high=5, default=1, space='buy') + intpar = IntParameter(low=0, high=5, default=1, space='buy') + assert intpar.value == 1 + assert isinstance(intpar.get_space(''), Integer) + + fltpar = FloatParameter(low=0.0, high=5.5, default=1.0, space='buy') + assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') + assert isinstance(catpar.get_space(''), Categorical) + assert catpar.value == 'buy_macd' + def test_auto_hyperopt_interface(default_conf): default_conf.update({'strategy': 'HyperoptableStrategy'}) @@ -587,3 +601,6 @@ def test_auto_hyperopt_interface(default_conf): # PlusDI is NOT in the buy-params, so default should be used assert strategy.buy_plusdi.value == 0.5 assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi'] + + # Parameter is disabled - so value from sell_param dict will NOT be used. + assert strategy.sell_minusdi.value == 0.5 From fc8478111e3d4b9810cee75857c68e3768390f82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 20:06:30 +0200 Subject: [PATCH 0076/1386] Improve strategy template --- freqtrade/templates/base_strategy.py.j2 | 5 +++-- freqtrade/templates/sample_strategy.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index dd6b773e1..db73d4da3 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -26,8 +26,9 @@ class {{ strategy }}(IStrategy): You must keep: - the lib in the section "Do not remove these libs" - - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, - populate_sell_trend, hyperopt_space, buy_strategy_generator + - the methods: populate_indicators, populate_buy_trend, populate_sell_trend + You should keep: + - timeframe, minimal_roi, stoploss, trailing_* """ # Strategy interface version - allow new iterations of the strategy interface. # Check the documentation or the Sample strategy to get the latest version. diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index db1ba48b8..5dfa42bcc 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -5,7 +5,7 @@ import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy # -------------------------------- # Add your lib to import here @@ -27,8 +27,9 @@ class SampleStrategy(IStrategy): You must keep: - the lib in the section "Do not remove these libs" - - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, - populate_sell_trend, hyperopt_space, buy_strategy_generator + - the methods: populate_indicators, populate_buy_trend, populate_sell_trend + You should keep: + - timeframe, minimal_roi, stoploss, trailing_* """ # Strategy interface version - allow new iterations of the strategy interface. # Check the documentation or the Sample strategy to get the latest version. From f6211bc00ebe43b4f8ae981658dc9f11a6c711aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 20:19:39 +0200 Subject: [PATCH 0077/1386] new-config should include API config --- freqtrade/commands/build_config_commands.py | 30 +++++++++++++++++++++ freqtrade/templates/base_config.json.j2 | 10 +++---- tests/commands/test_build_config.py | 4 +++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 3c34ff162..0dee480b3 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -1,4 +1,5 @@ import logging +import secrets from pathlib import Path from typing import Any, Dict, List @@ -138,6 +139,32 @@ def ask_user_config() -> Dict[str, Any]: "message": "Insert Telegram chat id", "when": lambda x: x['telegram'] }, + { + "type": "confirm", + "name": "api_server", + "message": "Do you want to enable the Rest API (includes FreqUI)?", + "default": False, + }, + { + "type": "text", + "name": "api_server_listen_addr", + "message": "Insert Api server Listen Address (best left untouched default!)", + "default": "127.0.0.1", + "when": lambda x: x['api_server'] + }, + { + "type": "text", + "name": "api_server_username", + "message": "Insert api-server username", + "default": "freqtrader", + "when": lambda x: x['api_server'] + }, + { + "type": "text", + "name": "api_server_password", + "message": "Insert api-server password", + "when": lambda x: x['api_server'] + }, ] answers = prompt(questions) @@ -145,6 +172,9 @@ def ask_user_config() -> Dict[str, Any]: # Interrupted questionary sessions return an empty dict. raise OperationalException("User interrupted interactive questions.") + # Force JWT token to be a random string + answers['api_server_jwt_key'] = secrets.token_hex() + return answers diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 226bf1a81..42f088f9f 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -54,15 +54,15 @@ "chat_id": "{{ telegram_chat_id }}" }, "api_server": { - "enabled": false, - "listen_ip_address": "127.0.0.1", + "enabled": {{ api_server | lower }}, + "listen_ip_address": "{{ api_server_listen_addr | default("127.0.0.1", true) }}", "listen_port": 8080, "verbosity": "error", "enable_openapi": false, - "jwt_secret_key": "somethingrandom", + "jwt_secret_key": "{{ api_server_jwt_key }}", "CORS_origins": [], - "username": "", - "password": "" + "username": "{{ api_server_username }}", + "password": "{{ api_server_password }}" }, "bot_name": "freqtrade", "initial_state": "running", diff --git a/tests/commands/test_build_config.py b/tests/commands/test_build_config.py index 291720f4b..66c750e79 100644 --- a/tests/commands/test_build_config.py +++ b/tests/commands/test_build_config.py @@ -50,6 +50,10 @@ def test_start_new_config(mocker, caplog, exchange): 'telegram': False, 'telegram_token': 'asdf1244', 'telegram_chat_id': '1144444', + 'api_server': False, + 'api_server_listen_addr': '127.0.0.1', + 'api_server_username': 'freqtrader', + 'api_server_password': 'MoneyMachine', } mocker.patch('freqtrade.commands.build_config_commands.ask_user_config', return_value=sample_selections) From 932284574077efca1c6cb1b578e2df48dc93540d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:27:22 +0000 Subject: [PATCH 0078/1386] Bump mkdocs-material from 7.0.6 to 7.0.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.6 to 7.0.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.0.6...7.0.7) 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 0068dd5d2..711b6ca46 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.6 +mkdocs-material==7.0.7 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From e5789b36cf8d6488abf1a8e0e2b71585c1893e9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:27:57 +0000 Subject: [PATCH 0079/1386] Bump numpy from 1.20.1 to 1.20.2 Bumps [numpy](https://github.com/numpy/numpy) from 1.20.1 to 1.20.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.20.1...v1.20.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..201ce22fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.20.1 +numpy==1.20.2 pandas==1.2.3 ccxt==1.43.89 From 607c05b3cec2523eec63b87639c64adec8bf928f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:28:16 +0000 Subject: [PATCH 0080/1386] Bump sqlalchemy from 1.4.2 to 1.4.3 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.2 to 1.4.3. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..ff5412fb7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.2 +SQLAlchemy==1.4.3 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 8e49271e6f948a37695438e8e57e43053e0c623a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:28:30 +0000 Subject: [PATCH 0081/1386] Bump prompt-toolkit from 3.0.17 to 3.0.18 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.17 to 3.0.18. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.17...3.0.18) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..127b9a776 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,4 +39,4 @@ aiofiles==0.6.0 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.17 +prompt-toolkit==3.0.18 From 95a9c92769c389fd44693ee8c6fdfa92755e43d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 09:13:48 +0200 Subject: [PATCH 0082/1386] Add permission-check before slack notify --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61ecaa522..dd6af0a06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,9 +310,17 @@ jobs: needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 steps: + - name: Check user permission + id: check + uses: scherermichael-oss/action-has-permission@1.0.6 + with: + required-permission: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Slack Notification uses: lazy-actions/slatify@v3.0.0 - if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} job_name: '*Freqtrade CI*' From 8d01767a421113c80870b392175effbfd3300def Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 09:20:34 +0200 Subject: [PATCH 0083/1386] Fix CI syntax --- .github/workflows/ci.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd6af0a06..8e15a5a89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,13 +310,14 @@ jobs: needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 steps: + - name: Check user permission - id: check - uses: scherermichael-oss/action-has-permission@1.0.6 - with: - required-permission: write - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + id: check + uses: scherermichael-oss/action-has-permission@1.0.6 + with: + required-permission: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Slack Notification uses: lazy-actions/slatify@v3.0.0 From dacaa4a732a396315eeb21f58301d7cccbef7056 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 09:34:57 +0000 Subject: [PATCH 0084/1386] Bump ccxt from 1.43.89 to 1.45.44 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.43.89 to 1.45.44. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.43.89...1.45.44) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10225f910..d3b8acb25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.43.89 +ccxt==1.45.44 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 From 3e864a87ad66b10058df601a4f7759dfe236ff70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 09:53:50 +0000 Subject: [PATCH 0085/1386] Bump cryptography from 3.4.6 to 3.4.7 Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.6 to 3.4.7. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.4.6...3.4.7) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10225f910..a57233823 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.2.3 ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels -cryptography==3.4.6 +cryptography==3.4.7 aiohttp==3.7.4.post0 SQLAlchemy==1.4.3 python-telegram-bot==13.4.1 From 5d5debab667facfb0bed113ed878392d4b52caeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 10:01:20 +0000 Subject: [PATCH 0086/1386] Bump scipy from 1.6.1 to 1.6.2 Bumps [scipy](https://github.com/scipy/scipy) from 1.6.1 to 1.6.2. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.6.1...v1.6.2) 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 8cdb6fd28..9eb490f83 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.6.1 +scipy==1.6.2 scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 From 6954a1e02951e587cee1d6b8e46365913ace34cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 19:27:19 +0200 Subject: [PATCH 0087/1386] MOre tests for ParameterHyperopt --- tests/optimize/conftest.py | 2 ++ tests/optimize/test_hyperopt.py | 17 +++++++++++++++++ tests/strategy/test_interface.py | 3 ++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index df6f22e01..5c789ec1e 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -14,6 +14,7 @@ from tests.conftest import patch_exchange def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ + 'datadir': Path(default_conf['datadir']), 'hyperopt': 'DefaultHyperOpt', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), @@ -21,6 +22,7 @@ def hyperopt_conf(default_conf): 'timerange': None, 'spaces': ['default'], 'hyperopt_jobs': 1, + 'hyperopt_min_trades': 1, }) return hyperconf diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 193d997db..36b6f1229 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -16,6 +16,7 @@ from freqtrade.commands.optimize_commands import setup_optimize_configuration, s from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -1089,3 +1090,19 @@ def test_print_epoch_details(capsys): assert '# ROI table:' in captured.out assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE) assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE) + + +def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: + # mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + # mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + # No hyperopt needed + del hyperopt_conf['hyperopt'] + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + }) + hyperopt = Hyperopt(hyperopt_conf) + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + + hyperopt.start() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 72b78338d..4d93f7049 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -587,7 +587,8 @@ def test_hyperopt_parameters(): assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 - catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], + default='buy_macd', space='buy') assert isinstance(catpar.get_space(''), Categorical) assert catpar.value == 'buy_macd' From 89bbfd2324ebf0974c60f1b8689941a079b14eea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 20:22:52 +0200 Subject: [PATCH 0088/1386] Remove candle_count from dataframe before backtesting closes #3754 --- freqtrade/data/converter.py | 16 +++++++++++----- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/optimize/hyperopt.py | 3 ++- tests/data/test_converter.py | 10 ++++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index d4053abaa..defc96ef8 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -115,17 +115,23 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) return df -def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date') -> DataFrame: +def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', + startup_candles: int = 0) -> DataFrame: """ Trim dataframe based on given timerange :param df: Dataframe to trim :param timerange: timerange (use start and end date if available) - :param: df_date_col: Column in the dataframe to use as Date column + :param df_date_col: Column in the dataframe to use as Date column + :param startup_candles: When not 0, is used instead the timerange start date :return: trimmed dataframe """ - if timerange.starttype == 'date': - start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) - df = df.loc[df[df_date_col] >= start, :] + if startup_candles: + # Trim candles instead of timeframe in case of given startup_candle count + df = df.iloc[startup_candles:, :] + else: + if timerange.starttype == 'date': + start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) + df = df.loc[df[df_date_col] >= start, :] if timerange.stoptype == 'date': stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc) df = df.loc[df[df_date_col] <= stop, :] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 765e2844a..ff1dd934c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -443,7 +443,8 @@ class Backtesting: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange) + preprocessed[pair] = trim_dataframe(df, timerange, + startup_candles=self.required_startup) min_date, max_date = history.get_timerange(preprocessed) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03f34a511..ee453489d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -379,7 +379,8 @@ class Hyperopt: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange) + preprocessed[pair] = trim_dataframe(df, timerange, + startup_candles=self.backtesting.required_startup) min_date, max_date = get_timerange(preprocessed) logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 4fdcce4d2..2420dca8f 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -197,6 +197,16 @@ def test_trim_dataframe(testdatadir) -> None: assert all(data_modify.iloc[-1] == data.iloc[-1]) assert all(data_modify.iloc[0] == data.iloc[30]) + data_modify = data.copy() + tr = TimeRange('date', None, min_date + 1800, 0) + # Remove first 20 candles - ignores min date + data_modify = trim_dataframe(data_modify, tr, startup_candles=20) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 20 + assert all(data_modify.iloc[-1] == data.iloc[-1]) + assert all(data_modify.iloc[0] == data.iloc[20]) + data_modify = data.copy() # Remove last 30 minutes (1800 s) tr = TimeRange(None, 'date', 0, max_date - 1800) From 50fcb3f330a16a477e3fdf48243f1d3accad6402 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Mar 2021 07:26:39 +0200 Subject: [PATCH 0089/1386] Reduce verbosity of missing data if less than 1% of data is missing --- freqtrade/data/converter.py | 9 ++++++++- tests/data/test_converter.py | 10 +++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index defc96ef8..c9d4ef19f 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -110,8 +110,15 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) df.reset_index(inplace=True) len_before = len(dataframe) len_after = len(df) + pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 if len_before != len_after: - logger.info(f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}") + message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}" + f" - {round(pct_missing * 100, 2)} %") + if pct_missing > 0.01: + logger.info(message) + else: + # Don't be verbose if only a small amount is missing + logger.debug(message) return df diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 2420dca8f..68960af1c 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -10,7 +10,7 @@ from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_forma trades_to_ohlcv, trim_dataframe) from freqtrade.data.history import (get_timerange, load_data, load_pair_history, validate_backtest_data) -from tests.conftest import log_has +from tests.conftest import log_has, log_has_re from tests.data.test_history import _backup_file, _clean_test_file @@ -62,8 +62,8 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog): # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " - f"{len(data)} - after: {len(data2)}", caplog) + assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: " + f"{len(data)} - after: {len(data2)}.*", caplog) # Test fillup actually fixes invalid backtest data min_date, max_date = get_timerange({'UNITTEST/BTC': data}) @@ -125,8 +125,8 @@ def test_ohlcv_fill_up_missing_data2(caplog): # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " - f"{len(data)} - after: {len(data2)}", caplog) + assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: " + f"{len(data)} - after: {len(data2)}.*", caplog) def test_ohlcv_drop_incomplete(caplog): From 2869d5368de5868b7dfb0c72d402449cf72c2997 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Mar 2021 20:20:24 +0200 Subject: [PATCH 0090/1386] Allow edge to use dynamic pairlists closes #4298 --- docs/edge.md | 6 +++--- freqtrade/configuration/config_validation.py | 5 ----- freqtrade/edge/edge_positioning.py | 5 ++--- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/edge_cli.py | 2 +- tests/edge/test_edge.py | 8 ++++---- tests/test_configuration.py | 16 ---------------- 7 files changed, 11 insertions(+), 33 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 5565ca2f9..0aa76cd12 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -1,9 +1,9 @@ # Edge positioning -The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss. +The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss. !!! Warning - `Edge positioning` is not compatible with dynamic (volume-based) whitelist. + WHen using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data. !!! Note `Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. @@ -14,7 +14,7 @@ The `Edge Positioning` module uses probability to calculate your win rate and ri Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. -To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money. +To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money. !!! tip "It doesn't matter how often, but how much!" A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit. diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index c7e49f33d..31e38d572 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -149,11 +149,6 @@ def _validate_edge(conf: Dict[str, Any]) -> None: if not conf.get('edge', {}).get('enabled'): return - if conf.get('pairlist', {}).get('method') == 'VolumePairList': - raise OperationalException( - "Edge and VolumePairList are incompatible, " - "Edge will override whatever pairs VolumePairlist selects." - ) if not conf.get('ask_strategy', {}).get('use_sell_signal', True): raise OperationalException( "Edge requires `use_sell_signal` to be True, otherwise no sells will happen." diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index ff86e522e..d1f76c21f 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -84,9 +84,8 @@ class Edge: self.fee = self.exchange.get_fee(symbol=expand_pairlist( self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) - def calculate(self) -> bool: - pairs = expand_pairlist(self.config['exchange']['pair_whitelist'], - list(self.exchange.markets)) + def calculate(self, pairs: List[str]) -> bool: + heartbeat = self.edge_config.get('process_throttle_secs') if (self._last_updated > 0) and ( diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 73f4c91be..dd6966848 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -225,7 +225,7 @@ class FreqtradeBot(LoggingMixin): # Calculating Edge positioning if self.edge: - self.edge.calculate() + self.edge.calculate(_whitelist) _whitelist = self.edge.adjust(_whitelist) if trades: diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index a5f505bee..aab7def05 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -44,7 +44,7 @@ class EdgeCli: 'timerange') is None else str(self.config.get('timerange'))) def start(self) -> None: - result = self.edge.calculate() + result = self.edge.calculate(self.config['exchange']['pair_whitelist']) if result: print('') # blank line for readability print(generate_edge_table(self.edge._cached_pairs)) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index c30bce6a4..5142dd985 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -266,7 +266,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): # should not recalculate if heartbeat not reached edge._last_updated = arrow.utcnow().int_timestamp - heartbeat + 1 - assert edge.calculate() is False + assert edge.calculate(edge_conf['exchange']['pair_whitelist']) is False def mocked_load_data(datadir, pairs=[], timeframe='0m', @@ -310,7 +310,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert edge.calculate() + assert edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 2 assert edge._last_updated <= arrow.utcnow().int_timestamp + 2 @@ -322,7 +322,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert not edge.calculate() + assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 0 assert log_has("No data found. Edge is stopped ...", caplog) assert edge._last_updated == 0 @@ -337,7 +337,7 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert not edge.calculate() + assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 0 assert log_has("No trades found.", caplog) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index a0824e65c..15fbab7f8 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -860,22 +860,6 @@ def test_validate_tsl(default_conf): validate_config_consistency(default_conf) -def test_validate_edge(edge_conf): - edge_conf.update({"pairlist": { - "method": "VolumePairList", - }}) - - with pytest.raises(OperationalException, - match="Edge and VolumePairList are incompatible, " - "Edge will override whatever pairs VolumePairlist selects."): - validate_config_consistency(edge_conf) - - edge_conf.update({"pairlist": { - "method": "StaticPairList", - }}) - validate_config_consistency(edge_conf) - - def test_validate_edge2(edge_conf): edge_conf.update({"ask_strategy": { "use_sell_signal": True, From 5e5b11d4d60c6f7f8544e8f1cd8064df01f18a6d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 31 Mar 2021 12:31:28 +0300 Subject: [PATCH 0091/1386] Split "enabled" to "load" and "optimize" parameters. --- freqtrade/optimize/hyperopt_auto.py | 8 ++++--- freqtrade/strategy/hyper.py | 36 ++++++++++++++++++----------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 847d8697a..0e3cf7eac 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -25,7 +25,8 @@ class HyperOptAuto(IHyperOpt): def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): - attr.value = params[attr_name] + if attr.optimize: + attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -33,7 +34,8 @@ class HyperOptAuto(IHyperOpt): def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): - attr.value = params[attr_name] + if attr.optimize: + attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) return populate_buy_trend @@ -53,7 +55,7 @@ class HyperOptAuto(IHyperOpt): def _generate_indicator_space(self, category): for attr_name, attr in self.strategy.enumerate_parameters(category): - if attr.enabled: + if attr.optimize: yield attr.get_space(attr_name) def _get_indicator_space(self, category, fallback_method_name): diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 64e457d75..c06ae36da 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -27,12 +27,14 @@ class BaseParameter(ABC): opt_range: Sequence[Any] def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, - enabled: bool = True, **kwargs): + optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ if 'name' in kwargs: @@ -42,7 +44,8 @@ class BaseParameter(ABC): self._space_params = kwargs self.value = default self.opt_range = opt_range - self.enabled = enabled + self.optimize = optimize + self.load = load def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -60,7 +63,7 @@ class IntParameter(BaseParameter): opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, - space: Optional[str] = None, enabled: bool = True, **kwargs): + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. @@ -69,6 +72,8 @@ class IntParameter(BaseParameter): :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Integer. """ if high is not None and isinstance(low, Sequence): @@ -79,8 +84,8 @@ class IntParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Integer': """ @@ -96,7 +101,7 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): + default: float, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. @@ -105,6 +110,8 @@ class FloatParameter(BaseParameter): :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Real. """ if high is not None and isinstance(low, Sequence): @@ -115,8 +122,8 @@ class FloatParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Real': """ @@ -132,7 +139,7 @@ class CategoricalParameter(BaseParameter): opt_range: Sequence[Any] def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, - space: Optional[str] = None, enabled: bool = True, **kwargs): + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param categories: Optimization space, [a, b, ...]. @@ -141,13 +148,15 @@ class CategoricalParameter(BaseParameter): :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Categorical. """ if len(categories) < 2: raise OperationalException( 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=categories, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Categorical': """ @@ -184,8 +193,7 @@ class HyperStrategyMixin(object): if issubclass(attr.__class__, BaseParameter): if category is None or category == attr.category or \ attr_name.startswith(category + '_'): - if attr.enabled: - yield attr_name, attr + yield attr_name, attr def _load_params(self, params: dict) -> None: """ @@ -196,7 +204,7 @@ class HyperStrategyMixin(object): return for attr_name, attr in self.enumerate_parameters(): if attr_name in params: - if attr.enabled: + if attr.load: attr.value = params[attr_name] logger.info(f'attr_name = {attr.value}') else: From 5acdc9bf424e6c46329a5f886182b5317321846a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 06:47:23 +0200 Subject: [PATCH 0092/1386] Fix type errors by converting all hyperopt methods to instance methods --- freqtrade/optimize/hyperopt_interface.py | 33 +++++++++--------------- freqtrade/strategy/hyper.py | 3 ++- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..e951efddd 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -44,36 +44,31 @@ class IHyperOpt(ABC): IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.timeframe = str(config['timeframe']) - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: """ Create a buy strategy generator. """ raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy')) - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: """ Create a sell strategy generator. """ raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell')) - @staticmethod - def indicator_space() -> List[Dimension]: + def indicator_space(self) -> List[Dimension]: """ Create an indicator space. """ raise OperationalException(_format_exception_message('indicator_space', 'buy')) - @staticmethod - def sell_indicator_space() -> List[Dimension]: + def sell_indicator_space(self) -> List[Dimension]: """ Create a sell indicator space. """ raise OperationalException(_format_exception_message('sell_indicator_space', 'sell')) - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: + def generate_roi_table(self, params: Dict) -> Dict[int, float]: """ Create a ROI table. @@ -88,8 +83,7 @@ class IHyperOpt(ABC): return roi_table - @staticmethod - def roi_space() -> List[Dimension]: + def roi_space(self) -> List[Dimension]: """ Create a ROI space. @@ -109,7 +103,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_min = timeframe_to_minutes(IHyperOpt.ticker_interval) + timeframe_min = timeframe_to_minutes(self.ticker_interval) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: @@ -145,7 +139,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_min'], 'roi_p3': roi_limits['roi_p3_min'], } - logger.info(f"Min roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}") + logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 5)}") p = { 'roi_t1': roi_limits['roi_t1_max'], 'roi_t2': roi_limits['roi_t2_max'], @@ -154,7 +148,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_max'], 'roi_p3': roi_limits['roi_p3_max'], } - logger.info(f"Max roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}") + logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 5)}") return [ Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), @@ -165,8 +159,7 @@ class IHyperOpt(ABC): Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'), ] - @staticmethod - def stoploss_space() -> List[Dimension]: + def stoploss_space(self) -> List[Dimension]: """ Create a stoploss space. @@ -177,8 +170,7 @@ class IHyperOpt(ABC): Real(-0.35, -0.02, name='stoploss'), ] - @staticmethod - def generate_trailing_params(params: Dict) -> Dict: + def generate_trailing_params(self, params: Dict) -> Dict: """ Create dict with trailing stop parameters. """ @@ -190,8 +182,7 @@ class IHyperOpt(ABC): 'trailing_only_offset_is_reached': params['trailing_only_offset_is_reached'], } - @staticmethod - def trailing_space() -> List[Dimension]: + def trailing_space(self) -> List[Dimension]: """ Create a trailing stoploss space. diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index c06ae36da..a6603ecbf 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -101,7 +101,8 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): + default: float, space: Optional[str] = None, + optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. From d64295ba24a13958d4d79c7dd8dfb6134b83b85b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 06:55:25 +0200 Subject: [PATCH 0093/1386] Adapt test strategy to new parameters --- tests/strategy/strats/hyperoptable_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 97d2092d4..a08293058 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -62,7 +62,7 @@ class HyperoptableStrategy(IStrategy): buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') - sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', enabled=False) + sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', load=False) def informative_pairs(self): """ From 51f0fcb2cb071ae2753c94a21e23c87b9a57e129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 12:20:38 +0200 Subject: [PATCH 0094/1386] Add profit_fiat to REST API --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 32a1c8597..eaca477d7 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -168,6 +168,7 @@ class TradeSchema(BaseModel): profit_ratio: Optional[float] profit_pct: Optional[float] profit_abs: Optional[float] + profit_fiat: Optional[float] sell_reason: Optional[str] sell_order_status: Optional[str] stop_loss_abs: Optional[float] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 62f1c2592..1b2dc5d66 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -173,6 +173,14 @@ class RPC: current_rate = NAN current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) + + # Calculate fiat profit + current_profit_fiat = self._fiat_converter.convert_amount( + current_profit_abs, + self._freqtrade.config['stake_currency'], + self._freqtrade.config['fiat_display_currency'] + ) + # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss) @@ -191,6 +199,7 @@ class RPC: profit_ratio=current_profit, profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, + profit_fiat=current_profit_fiat, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), From 2c0079b00b8902405f487f1c8ffbc1f614de8651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 13:16:52 +0200 Subject: [PATCH 0095/1386] Add profit_fiat to tests, use ANY, as price changes... --- tests/rpc/test_rpc_apiserver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5a0a04943..3b1ef6a74 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -786,6 +786,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, + 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, 'open_date_hum': 'just now', From f47dc317867ebe5186519d99e4ee5d3b0ffab2d3 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:35 +0530 Subject: [PATCH 0096/1386] Refactor the comparison involving `not` Signed-off-by: shubhendra --- .deepsource.toml | 16 ---------------- freqtrade/exchange/exchange.py | 2 +- freqtrade/plugins/pairlist/PerformanceFilter.py | 2 +- freqtrade/plugins/protections/cooldown_period.py | 1 - freqtrade/resolvers/strategy_resolver.py | 4 ++-- 5 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 7a00ca8d6..000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,16 +0,0 @@ -version = 1 - -test_patterns = ["tests/**/test_*.py"] - -exclude_patterns = [ - "docs/**", - "user_data/**", - "build/helpers/**" -] - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x.x" diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9c868df2b..85c5b4c6d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -806,7 +806,7 @@ class Exchange: # Gather coroutines to run for pair, timeframe in set(pair_list): - if (not ((pair, timeframe) in self._klines) + if (((pair, timeframe) not in self._klines) or self._now_is_time_to_refresh(pair, timeframe)): input_coroutines.append(self._async_get_candle_history(pair, timeframe, since_ms=since_ms)) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index c1355f655..73a9436fa 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,7 +2,7 @@ Performance pair list filter """ import logging -from typing import Any, Dict, List +from typing import Dict, List import pandas as pd diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 197d74c2e..a2d8eca34 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -1,7 +1,6 @@ import logging from datetime import datetime, timedelta -from typing import Any, Dict from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 19bd014f9..05fbac10d 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -197,8 +197,8 @@ class StrategyResolver(IResolver): strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) if any(x == 2 for x in [strategy._populate_fun_len, - strategy._buy_fun_len, - strategy._sell_fun_len]): + strategy._buy_fun_len, + strategy._sell_fun_len]): strategy.INTERFACE_VERSION = 1 return strategy From ede26091b980a22d84bc44ce5e5e3b74119ba76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 14:35:19 +0200 Subject: [PATCH 0097/1386] Add validation in the right places... --- tests/rpc/test_rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 1 + 2 files changed, 3 insertions(+) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index b11470711..64918ed47 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -92,6 +92,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, + 'profit_fiat': ANY, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, @@ -159,6 +160,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, + 'profit_fiat': ANY, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3b1ef6a74..20d32024f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -966,6 +966,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'profit_ratio': None, 'profit_pct': None, 'profit_abs': None, + 'profit_fiat': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, From 3691ae8686d9987966956018aa8992b36278ce22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 14:50:47 +0200 Subject: [PATCH 0098/1386] Make sure the fiat converter exists before calling it --- freqtrade/rpc/rpc.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1b2dc5d66..1359729b9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -175,11 +175,12 @@ class RPC: current_profit_abs = trade.calc_profit(current_rate) # Calculate fiat profit - current_profit_fiat = self._fiat_converter.convert_amount( - current_profit_abs, - self._freqtrade.config['stake_currency'], - self._freqtrade.config['fiat_display_currency'] - ) + if self._fiat_converter: + current_profit_fiat = self._fiat_converter.convert_amount( + current_profit_abs, + self._freqtrade.config['stake_currency'], + self._freqtrade.config['fiat_display_currency'] + ) # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) From ea43d5ba85caf490604d02a6abae8e620324f034 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 1 Apr 2021 10:17:39 +0300 Subject: [PATCH 0099/1386] Implement DecimalParameter and rename FloatParameter to RealParameter. --- freqtrade/optimize/hyperopt_auto.py | 6 +- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/hyper.py | 66 +++++++++++++++++-- .../strategy/strats/hyperoptable_strategy.py | 7 +- tests/strategy/test_interface.py | 26 ++++++-- 5 files changed, 88 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 0e3cf7eac..ed6f2d6f7 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -26,7 +26,8 @@ class HyperOptAuto(IHyperOpt): def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): if attr.optimize: - attr.value = params[attr_name] + # noinspection PyProtectedMember + attr._set_value(params[attr_name]) return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -35,7 +36,8 @@ class HyperOptAuto(IHyperOpt): def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: - attr.value = params[attr_name] + # noinspection PyProtectedMember + attr._set_value(params[attr_name]) return self.strategy.populate_sell_trend(dataframe, metadata) return populate_buy_trend diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index bc0c45f7c..bd49165df 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa: F401 from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) -from freqtrade.strategy.hyper import CategoricalParameter, FloatParameter, IntParameter +from freqtrade.strategy.hyper import (CategoricalParameter, DecimalParameter, IntParameter, + RealParameter) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index a6603ecbf..e58aac273 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -56,6 +56,14 @@ class BaseParameter(ABC): Get-space - will be used by Hyperopt to get the hyperopt Space """ + def _set_value(self, value: Any): + """ + Update current value. Used by hyperopt functions for the purpose where optimization and + value spaces differ. + :param value: A numerical value. + """ + self.value = value + class IntParameter(BaseParameter): default: int @@ -65,7 +73,7 @@ class IntParameter(BaseParameter): def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ - Initialize hyperopt-optimizable parameter. + Initialize hyperopt-optimizable integer parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. :param high: Upper end (inclusive) of optimization space. Must be none of entire range is passed first parameter. @@ -95,16 +103,16 @@ class IntParameter(BaseParameter): return Integer(*self.opt_range, name=name, **self._space_params) -class FloatParameter(BaseParameter): +class RealParameter(BaseParameter): default: float value: float opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, - optimize: bool = True, load: bool = True, **kwargs): + default: float, space: Optional[str] = None, optimize: bool = True, + load: bool = True, **kwargs): """ - Initialize hyperopt-optimizable parameter. + Initialize hyperopt-optimizable floating point parameter with unlimited precision. :param low: Lower end (inclusive) of optimization space or [low, high]. :param high: Upper end (inclusive) of optimization space. Must be none if entire range is passed first parameter. @@ -116,10 +124,10 @@ class FloatParameter(BaseParameter): :param kwargs: Extra parameters to skopt.space.Real. """ if high is not None and isinstance(low, Sequence): - raise OperationalException('FloatParameter space invalid.') + raise OperationalException(f'{self.__class__.__name__} space invalid.') if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException('FloatParameter space must be [low, high]') + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') opt_range = low else: opt_range = [low, high] @@ -134,6 +142,50 @@ class FloatParameter(BaseParameter): return Real(*self.opt_range, name=name, **self._space_params) +class DecimalParameter(RealParameter): + default: float + value: float + opt_range: Sequence[float] + + def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, + default: float, decimals: int = 3, space: Optional[str] = None, + optimize: bool = True, load: bool = True, **kwargs): + """ + Initialize hyperopt-optimizable decimal parameter with a limited precision. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none if entire range is passed first parameter. + :param default: A default value. + :param decimals: A number of decimals after floating point to be included in testing. + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. + :param kwargs: Extra parameters to skopt.space.Real. + """ + self._decimals = decimals + default = round(default, self._decimals) + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, + load=load, **kwargs) + + def get_space(self, name: str) -> 'Integer': + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ + low = int(self.opt_range[0] * pow(10, self._decimals)) + high = int(self.opt_range[1] * pow(10, self._decimals)) + return Integer(low, high, name=name, **self._space_params) + + def _set_value(self, value: int): + """ + Update current value. Used by hyperopt functions for the purpose where optimization and + value spaces differ. + :param value: An integer value. + """ + self.value = round(value * pow(0.1, self._decimals), self._decimals) + + class CategoricalParameter(BaseParameter): default: Any value: Any diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index a08293058..cc4734e13 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy import FloatParameter, IntParameter, IStrategy +from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, RealParameter class HyperoptableStrategy(IStrategy): @@ -60,9 +60,10 @@ class HyperoptableStrategy(IStrategy): } buy_rsi = IntParameter([0, 50], default=30, space='buy') - buy_plusdi = FloatParameter(low=0, high=1, default=0.5, 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 = FloatParameter(low=0, high=1, default=0.5, space='sell', load=False) + sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) def informative_pairs(self): """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4d93f7049..71f877cc3 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,8 +13,8 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, FloatParameter, - IntParameter) +from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter, + IntParameter, RealParameter) from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -564,14 +564,20 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"IntParameter space must be.*"): IntParameter(low=0, default=5, space='buy') - with pytest.raises(OperationalException, match=r"FloatParameter space must be.*"): - FloatParameter(low=0, default=5, space='buy') + with pytest.raises(OperationalException, match=r"RealParameter space must be.*"): + RealParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"DecimalParameter space must be.*"): + DecimalParameter(low=0, default=5, space='buy') with pytest.raises(OperationalException, match=r"IntParameter space invalid\."): IntParameter([0, 10], high=7, default=5, space='buy') - with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): - FloatParameter([0, 10], high=7, default=5, space='buy') + with pytest.raises(OperationalException, match=r"RealParameter space invalid\."): + RealParameter([0, 10], high=7, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"DecimalParameter space invalid\."): + DecimalParameter([0, 10], high=7, default=5, space='buy') with pytest.raises(OperationalException, match=r"CategoricalParameter space must.*"): CategoricalParameter(['aa'], default='aa', space='buy') @@ -583,10 +589,16 @@ def test_hyperopt_parameters(): assert intpar.value == 1 assert isinstance(intpar.get_space(''), Integer) - fltpar = FloatParameter(low=0.0, high=5.5, default=1.0, space='buy') + fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy') assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 + fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') + assert isinstance(fltpar.get_space(''), Integer) + assert fltpar.value == 1 + fltpar._set_value(2222) + assert fltpar.value == 2.222 + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') assert isinstance(catpar.get_space(''), Categorical) From 7728e269fdcae6b078913628a067edb5cbcdfd7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 07:17:02 +0200 Subject: [PATCH 0100/1386] Include Technical in default image --- docker-compose.yml | 2 +- docker/{Dockerfile.technical => Dockerfile.custom} | 3 ++- docs/docker_quickstart.md | 4 ++-- requirements.txt | 1 + setup.py | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) rename docker/{Dockerfile.technical => Dockerfile.custom} (50%) diff --git a/docker-compose.yml b/docker-compose.yml index 1f63059f0..80e194ab2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: # Build step - only needed when additional dependencies are needed # build: # context: . - # dockerfile: "./docker/Dockerfile.technical" + # dockerfile: "./docker/Dockerfile.custom" restart: unless-stopped container_name: freqtrade volumes: diff --git a/docker/Dockerfile.technical b/docker/Dockerfile.custom similarity index 50% rename from docker/Dockerfile.technical rename to docker/Dockerfile.custom index 9431e72d0..10620e6b8 100644 --- a/docker/Dockerfile.technical +++ b/docker/Dockerfile.custom @@ -3,4 +3,5 @@ FROM freqtradeorg/freqtrade:develop RUN apt-get update \ && apt-get -y install git \ && apt-get clean \ - && pip install git+https://github.com/freqtrade/technical + # The below dependency - pyti - serves as an example. Please use whatever you need! + && pip install pyti diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 017264569..9e74841b4 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -156,8 +156,8 @@ Head over to the [Backtesting Documentation](backtesting.md) to learn more. ### Additional dependencies with docker-compose -If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) for an example). +If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host. +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.cusotm) for an example). You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. diff --git a/requirements.txt b/requirements.txt index 93ed7570e..e4984cf47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 +technical==1.2.2 tabulate==0.8.9 pycoingecko==1.4.0 jinja2==2.11.3 diff --git a/setup.py b/setup.py index 118bc8485..bf23fb999 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ setup(name='freqtrade', 'wrapt', 'jsonschema', 'TA-Lib', + 'technical', 'tabulate', 'pycoingecko', 'py_find_1st', From e7a1924aa0e1c9c795516db10690af8fbccffb37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 08:36:06 +0200 Subject: [PATCH 0101/1386] Fix typo --- docs/docker_quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 9e74841b4..ca0515281 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -157,7 +157,7 @@ Head over to the [Backtesting Documentation](backtesting.md) to learn more. ### Additional dependencies with docker-compose If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.cusotm) for an example). +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.custom) for an example). You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. From 23c19b6852f62026f80366bde35ad7f8254f61b1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 3 Apr 2021 11:17:18 +0300 Subject: [PATCH 0102/1386] New hyperopt documentation. --- docs/hyperopt.md | 283 +++++++++--------- docs/hyperopt_legacy.md | 629 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 759 insertions(+), 153 deletions(-) create mode 100644 docs/hyperopt_legacy.md diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 69bc57d1a..e9d440a5a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -14,6 +14,9 @@ To learn how to get data for the pairs and exchange you're interested in, head o !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) +!!! Note + Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + ## Install hyperopt dependencies Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. @@ -137,47 +140,19 @@ Strategy arguments: ``` -## Prepare Hyperopting - -Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). - -Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. - -!!! Tip "About this page" - For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. - -The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - ### Hyperopt checklist Checklist on all tasks / possibilities in hyperopt Depending on the space you want to optimize, only some of the below are required: -* fill `buy_strategy_generator` - for buy signal optimization -* fill `indicator_space` - for buy signal optimization -* fill `sell_strategy_generator` - for sell signal optimization -* fill `sell_indicator_space` - for sell signal optimization +* define parameters with `space='buy'` - for buy signal optimization +* define parameters with `space='sell'` - for sell signal optimization !!! Note `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. -Optional in hyperopt - can also be loaded from a strategy (recommended): - -* `populate_indicators` - fallback to create indicators -* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy -* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy - -!!! Note - You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. - Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. - -Rarely you may also need to override: +Rarely you may also need to create a nested class named `HyperOpt` and implement: * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) @@ -185,31 +160,19 @@ Rarely you may also need to override: * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" - You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. + You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. ```python # Have a working strategy at hand. - freqtrade new-hyperopt --hyperopt EmptyHyperopt - - freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 + freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 ``` -### Create a Custom Hyperopt File - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -This command will create a new hyperopt file from a template, allowing you to get started quickly. - ### Configure your Guards and Triggers -There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: +There are two places you need to change in your strategy file to add a new buy hyperopt for testing: -* Inside `indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. +* Define the parameters at the class level hyperopt shall be optimizing. +* Within `populate_buy_trend()` - use defined parameter values instead of raw constants. There you have two different types of indicators: 1. `guards` and 2. `triggers`. @@ -225,24 +188,46 @@ Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if ADX > 10*". -If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. +```python +from freqtrade.strategy import IntParameter, IStrategy + +class MyAwesomeStrategy(IStrategy): + # If parameter is prefixed with `buy_` or `sell_` then specifying `space` parameter is optional + # and space is inferred from parameter name. + buy_adx_min = IntParameter(0, 100, default=10) + + def populate_buy_trend(self, dataframe: 'DataFrame', metadata: dict) -> 'DataFrame': + dataframe.loc[ + ( + (dataframe['adx'] > self.buy_adx_min.value) + ), 'buy'] = 1 + return dataframe +``` #### Sell optimization Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods -* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. +* Define the parameters at the class level hyperopt shall be optimizing. +* Within `populate_sell_trend()` - use defined parameter values instead of raw constants. The configuration and rules are the same than for buy signals. -To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. -#### Using timeframe as a part of the Strategy +```python +class MyAwesomeStrategy(IStrategy): + # There is no strict parameter naming scheme. If you do not use `buy_` or `sell_` prefixes - + # please specify to which space parameter belongs using `space` parameter. Possible values: + # 'buy' or 'sell'. + adx_max = IntParameter(0, 100, default=50, space='sell') -The Strategy class exposes the timeframe value as the `self.timeframe` attribute. -The same value is available as class-attribute `HyperoptName.timeframe`. -In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe['adx'] < self.adx_max.value) + ), 'buy'] = 1 + return dataframe +``` ## Solving a Mystery @@ -252,25 +237,20 @@ help with those buy decisions. If you decide to use RSI or ADX, which values should I use for them? So let's use hyperparameter optimization to solve this mystery. -We will start by defining a search space: +We will start by defining hyperoptable parameters: ```python - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching strategy parameters - """ - return [ - Integer(20, 40, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') - ] +class MyAwesomeStrategy(IStrategy): + buy_adx = IntParameter(20, 40, default=30) + buy_rsi = IntParameter(20, 40, default=30) + buy_adx_enabled = CategoricalParameter([True, False]), + buy_rsi_enabled = CategoricalParameter([True, False]), + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), ``` Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` -and `rsi-value`) and I want you test in the range of values 20 to 40. +to find the best combination. Two of them are integer values (`buy_adx` +and `buy_rsi`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. @@ -278,39 +258,31 @@ one we call `trigger` and use it to decide which buy trigger we want to use. So let's write the buy strategy using these values: ```python - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if self.buy_adx_enabled.value: + conditions.append(dataframe['adx'] > self.buy_adx.value) + if self.buy_rsi_enabled.value: + conditions.append(dataframe['rsi'] < self.buy_rsi.value) - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) + # TRIGGERS + if self.buy_trigger.value == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if self.buy_trigger.value == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 - return dataframe - - return populate_buy_trend + return dataframe ``` Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. @@ -322,6 +294,20 @@ Based on the results, hyperopt will tell you which parameter combination produce When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in your strategy or hyperopt file. +## Parameter types + +There are four parameter types each suited for different purposes. +* `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. +* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. +* `RealParameter` - defines a floating point parameter with upper and lower boundarie and no precision limit. Rarely used. +* `CategoricalParameter` - defines a parameter with a predetermined number of choices. + +!!! Tip "Disabling parameter optimization" + Each parameter takes two boolean parameters: + * `load` - when set to `False` it will not load values configured in `buy_params` and `sell_params`. + * `optimize` - when set to `False` parameter will not be included in optimization process. + Use these parameters to quickly prototype various ideas. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. @@ -348,11 +334,9 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +freqtrade hyperopt --config config.json --hyperopt-loss --strategy -e 500 --spaces all ``` -Use `` as the name of the custom hyperopt used. - The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. @@ -378,14 +362,6 @@ For example, to use one month of data, pass the following parameter to the hyper freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 ``` -### Running Hyperopt using methods from a strategy - -Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. - -```bash -freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy -``` - ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. @@ -439,7 +415,7 @@ If you have not changed anything in the command line options, configuration, tim ## Understand the Hyperopt Result -Once Hyperopt is completed you can use the result to create a new strategy. +Once Hyperopt is completed you can use the result to update your strategy. Given the following result from hyperopt: ``` @@ -447,40 +423,36 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } ``` You should understand this result like: - The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `adx-enabled: False`) -- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) +- You should not use ADX because `'buy_adx_enabled': False`) +- You should **consider** using the RSI indicator (`'buy_rsi_enabled': True` and the best value is `29.0` (`'buy_rsi': 29.0`) -You have to look inside your strategy file into `buy_strategy_generator()` -method, what those values match to. +Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste it at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. -So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: +Transferring your whole hyperopt result to your strategy would then look like: ```python -(dataframe['rsi'] < 29.0) -``` - -Translating your whole hyperopt result as the new buy-signal would then look like: - -```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - dataframe.loc[ - ( - (dataframe['rsi'] < 29.0) & # rsi-value - dataframe['close'] < dataframe['bb_lowerband'] # trigger - ), - 'buy'] = 1 - return dataframe +class MyAwsomeStrategy(IStrategy): + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } ``` By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. @@ -499,11 +471,13 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -ROI table: -{ 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0} + # ROI table: + minimal_roi = { + 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0 + } ``` In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: @@ -549,13 +523,16 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -Stoploss: -0.27996 + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } + + stoploss: -0.27996 ``` In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: @@ -585,11 +562,11 @@ Best result: 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 -Trailing stop: -{ 'trailing_only_offset_is_reached': True, - 'trailing_stop': True, - 'trailing_stop_positive': 0.02001, - 'trailing_stop_positive_offset': 0.06038} + # Trailing stop: + trailing_stop = True + trailing_stop_positive = 0.02001 + trailing_stop_positive_offset = 0.06038 + trailing_only_offset_is_reached = True ``` In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md new file mode 100644 index 000000000..8c6972b5f --- /dev/null +++ b/docs/hyperopt_legacy.md @@ -0,0 +1,629 @@ +# Legacy Hyperopt + +This page explains how to tune your strategy by finding the optimal +parameters, a process called hyperparameter optimization. The bot uses several +algorithms included in the `scikit-optimize` package to accomplish this. The +search will burn all your CPU cores, make your laptop sound like a fighter jet +and still take a long time. + +In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). + +Hyperopt requires historic data to be available, just as backtesting does. +To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. + +!!! Note + Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Please update your strategy class following new documentation at [Hyperopt](hyperopt.md). + +!!! Bug +Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) + +## Install hyperopt dependencies + +Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. + +!!! Note +Since Hyperopt is a resource intensive process, running it on a Raspberry Pi is not recommended nor supported. + +### Docker + +The docker-image includes hyperopt dependencies, no further action needed. + +### Easy installation script (setup.sh) / Manual installation + +```bash +source .env/bin/activate +pip install -r requirements-hyperopt.txt +``` + +## Hyperopt command reference + + +``` +usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + [-i TIMEFRAME] [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] + [--max-open-trades INT] + [--stake-amount STAKE_AMOUNT] [--fee FLOAT] + [--hyperopt NAME] [--hyperopt-path PATH] [--eps] + [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [-e INT] + [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] + [--print-all] [--no-color] [--print-json] [-j JOBS] + [--random-state INT] [--min-trades INT] + [--hyperopt-loss NAME] + +optional arguments: + -h, --help show this help message and exit + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). + --timerange TIMERANGE + Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. + --fee FLOAT Specify fee ratio. Will be applied twice (on trade + entry and exit). + --hyperopt NAME Specify hyperopt class name which will be used by the + bot. + --hyperopt-path PATH Specify additional lookup path for Hyperopt and + Hyperopt Loss functions. + --eps, --enable-position-stacking + Allow buying the same pair multiple times (position + stacking). + --dmmp, --disable-max-market-positions + Disable applying `max_open_trades` during backtest + (same as setting `max_open_trades` to a very high + number). + --enable-protections, --enableprotections + Enable protections for backtesting.Will slow + backtesting down by a considerable amount, but will + include configured protections + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. + -e INT, --epochs INT Specify number of epochs (default: 100). + --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] + Specify which parameters to hyperopt. Space-separated + list. + --print-all Print all results, not only the best ones. + --no-color Disable colorization of hyperopt results. May be + useful if you are redirecting output to a file. + --print-json Print output in JSON format. + -j JOBS, --job-workers JOBS + The number of concurrently running jobs for + hyperoptimization (hyperopt worker processes). If -1 + (default), all CPUs are used, for -2, all CPUs but one + are used, etc. If 1 is given, no parallel computing + code is used at all. + --random-state INT Set random state to some positive integer for + reproducible hyperopt results. + --min-trades INT Set minimal desired number of trades for evaluations + in the hyperopt optimization path (default: 1). + --hyperopt-loss NAME Specify the class name of the hyperopt loss function + class (IHyperOptLoss). Different functions can + generate completely different results, since the + target for optimization is different. Built-in + Hyperopt-loss-functions are: + ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, + SharpeHyperOptLoss, SharpeHyperOptLossDaily, + SortinoHyperOptLoss, SortinoHyperOptLossDaily + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --strategy-path PATH Specify additional strategy lookup path. + +``` + +## Prepare Hyperopting + +Before we start digging into Hyperopt, we recommend you to take a look at +the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). + +Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. + +!!! Tip "About this page" +For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + +The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +### Hyperopt checklist + +Checklist on all tasks / possibilities in hyperopt + +Depending on the space you want to optimize, only some of the below are required: + +* fill `buy_strategy_generator` - for buy signal optimization +* fill `indicator_space` - for buy signal optimization +* fill `sell_strategy_generator` - for sell signal optimization +* fill `sell_indicator_space` - for sell signal optimization + +!!! Note +`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + +Optional in hyperopt - can also be loaded from a strategy (recommended): + +* `populate_indicators` - fallback to create indicators +* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy +* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy + +!!! Note +You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. +Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) +* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) + +!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" +You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. + + ```python + # Have a working strategy at hand. + freqtrade new-hyperopt --hyperopt EmptyHyperopt + + freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 + ``` + +### Create a Custom Hyperopt File + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +This command will create a new hyperopt file from a template, allowing you to get started quickly. + +### Configure your Guards and Triggers + +There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: + +* Inside `indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. + +There you have two different types of indicators: 1. `guards` and 2. `triggers`. + +1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. +2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". + +!!! Hint "Guards and Triggers" +Technically, there is no difference between Guards and Triggers. +However, this guide will make this distinction to make it clear that signals should not be "sticking". +Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). + +Hyper-optimization will, for each epoch round, pick one trigger and possibly +multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if +ADX > 10*". + +If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. + +#### Sell optimization + +Similar to the buy-signal above, sell-signals can also be optimized. +Place the corresponding settings into the following methods + +* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. + +The configuration and rules are the same than for buy signals. +To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. + +#### Using timeframe as a part of the Strategy + +The Strategy class exposes the timeframe value as the `self.timeframe` attribute. +The same value is available as class-attribute `HyperoptName.timeframe`. +In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. + +## Solving a Mystery + +Let's say you are curious: should you use MACD crossings or lower Bollinger +Bands to trigger your buys. And you also wonder should you use RSI or ADX to +help with those buy decisions. If you decide to use RSI or ADX, which values +should I use for them? So let's use hyperparameter optimization to solve this +mystery. + +We will start by defining a search space: + +```python + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching strategy parameters + """ + return [ + Integer(20, 40, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') + ] +``` + +Above definition says: I have five parameters I want you to randomly combine +to find the best combination. Two of them are integer values (`adx-value` +and `rsi-value`) and I want you test in the range of values 20 to 40. +Then we have three category variables. First two are either `True` or `False`. +We use these to either enable or disable the ADX and RSI guards. The last +one we call `trigger` and use it to decide which buy trigger we want to use. + +So let's write the buy strategy using these values: + +```python + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] > params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] < params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend +``` + +Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. +It will use the given historical data and make buys based on the buy signals generated with the above function. +Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). + +!!! Note +The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. +When you want to test an indicator that isn't used by the bot currently, remember to +add it to the `populate_indicators()` method in your strategy or hyperopt file. + +## Loss-functions + +Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. + +A loss function must be specified via the `--hyperopt-loss ` argument (or optionally via the configuration under the `"hyperopt_loss"` key). +This class should be in its own file within the `user_data/hyperopts/` directory. + +Currently, the following loss functions are builtin: + +* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. +* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) +* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) +* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) +* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) +* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) + +Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. + +## Execute Hyperopt + +Once you have updated your hyperopt configuration you can run it. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. + +We strongly recommend to use `screen` or `tmux` to prevent any connection loss. + +```bash +freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +``` + +Use `` as the name of the custom hyperopt used. + +The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. +Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. + +The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. + +!!! Note +Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. +Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. +You can find a list of filenames with `ls -l user_data/hyperopt_results/`. + +### Execute Hyperopt with different historical data source + +If you would like to hyperopt parameters using an alternate historical data set that +you have on-disk, use the `--datadir PATH` option. By default, hyperopt +uses data from directory `user_data/data`. + +### Running Hyperopt with a smaller test-set + +Use the `--timerange` argument to change how much of the test-set you want to use. +For example, to use one month of data, pass the following parameter to the hyperopt call: + +```bash +freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 +``` + +### Running Hyperopt using methods from a strategy + +Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. + +```bash +freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy +``` + +### Running Hyperopt with Smaller Search Space + +Use the `--spaces` option to limit the search space used by hyperopt. +Letting Hyperopt optimize everything is a huuuuge search space. +Often it might make more sense to start by just searching for initial buy algorithm. +Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. + +Legal values are: + +* `all`: optimize everything +* `buy`: just search for a new buy strategy +* `sell`: just search for a new sell strategy +* `roi`: just optimize the minimal profit table for your strategy +* `stoploss`: search for the best stoploss value +* `trailing`: search for the best trailing stop values +* `default`: `all` except `trailing` +* space-separated list of any of the above values for example `--spaces roi stoploss` + +The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. + +### Position stacking and disabling max market positions + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previously open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high +number). + +!!! Note +Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. + +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. + +## Understand the Hyperopt Result + +Once Hyperopt is completed you can use the result to create a new strategy. +Given the following result from hyperopt: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +``` + +You should understand this result like: + +- The buy trigger that worked best was `bb_lower`. +- You should not use ADX because `adx-enabled: False`) +- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) + +You have to look inside your strategy file into `buy_strategy_generator()` +method, what those values match to. + +So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: + +```python +(dataframe['rsi'] < 29.0) +``` + +Translating your whole hyperopt result as the new buy-signal would then look like: + +```python +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + (dataframe['rsi'] < 29.0) & # rsi-value + dataframe['close'] < dataframe['bb_lowerband'] # trigger + ), + 'buy'] = 1 + return dataframe +``` + +By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. + +You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. + +!!! Note "Windows and color output" +Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + +### Understand Hyperopt ROI results + +If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +ROI table: +{ 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0} +``` + +In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: + +``` + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0 + } +``` + +As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file. + +#### Default ROI Search Space + +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): + +| # step | 1m | | 5m | | 1h | | 1d | | +| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | +| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | +| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | +| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | +| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | + +These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. + +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. + +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). + +A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +### Understand Hyperopt Stoploss results + +If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +Stoploss: -0.27996 +``` + +In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: + +``` python + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.27996 +``` + +As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. + +#### Default Stoploss Search Space + +If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace vary in the range -0.35...-0.02, which is sufficient in most cases. + +If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. + +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +### Understand Hyperopt Trailing Stop results + +If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: + +``` +Best result: + + 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 + +Trailing stop: +{ 'trailing_only_offset_is_reached': True, + 'trailing_stop': True, + 'trailing_stop_positive': 0.02001, + 'trailing_stop_positive_offset': 0.06038} +``` + +In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: + +``` python + # Trailing stop + # These attributes will be overridden if the config file contains corresponding values. + trailing_stop = True + trailing_stop_positive = 0.02001 + trailing_stop_positive_offset = 0.06038 + trailing_only_offset_is_reached = True +``` + +As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file. + +#### Default Trailing Stop Search Space + +If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. + +Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +## Show details of Hyperopt results + +After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. + +## Validate backtesting results + +Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. + +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. + +Should results don't match, please double-check to make sure you transferred all conditions correctly. +Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. +You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). From faf40482ef9bebf9cbb5469f17b4d136fded3ced Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 3 Apr 2021 13:49:24 +0300 Subject: [PATCH 0103/1386] Fix parameter printing. --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e58aac273..6282d91c0 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -259,7 +259,7 @@ class HyperStrategyMixin(object): if attr_name in params: if attr.load: attr.value = params[attr_name] - logger.info(f'attr_name = {attr.value}') + logger.info(f'{attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') From 4eb7ce52cd61fcd14f546b72be3b45ee03bcc8a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 15:15:38 +0200 Subject: [PATCH 0104/1386] Remove duplicate entries from hyperopt_legacy --- docs/hyperopt.md | 47 ++--- docs/hyperopt_legacy.md | 446 ++++------------------------------------ 2 files changed, 62 insertions(+), 431 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e9d440a5a..725afebf3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -1,21 +1,21 @@ # Hyperopt This page explains how to tune your strategy by finding the optimal -parameters, a process called hyperparameter optimization. The bot uses several -algorithms included in the `scikit-optimize` package to accomplish this. The -search will burn all your CPU cores, make your laptop sound like a fighter jet -and still take a long time. +parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `scikit-optimize` package to accomplish this. +The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time. In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). -Hyperopt requires historic data to be available, just as backtesting does. +Hyperopt requires historic data to be available, just as backtesting does (hyperopt runs backtesting many times with different parameters). To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) !!! Note - Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy. + The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt. + The legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). ## Install hyperopt dependencies @@ -37,7 +37,6 @@ pip install -r requirements-hyperopt.txt ## Hyperopt command reference - ``` usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] @@ -150,7 +149,7 @@ Depending on the space you want to optimize, only some of the below are required * define parameters with `space='sell'` - for sell signal optimization !!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. Rarely you may also need to create a nested class named `HyperOpt` and implement: @@ -299,7 +298,7 @@ Based on the results, hyperopt will tell you which parameter combination produce There are four parameter types each suited for different purposes. * `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. * `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. -* `RealParameter` - defines a floating point parameter with upper and lower boundarie and no precision limit. Rarely used. +* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities. * `CategoricalParameter` - defines a parameter with a predetermined number of choices. !!! Tip "Disabling parameter optimization" @@ -329,7 +328,7 @@ Creation of a custom loss function is covered in the [Advanced Hyperopt](advance ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. We strongly recommend to use `screen` or `tmux` to prevent any connection loss. @@ -365,7 +364,7 @@ freqtrade hyperopt --hyperopt --strategy --timeran ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. +Letting Hyperopt optimize everything is a huuuuge search space. Often it might make more sense to start by just searching for initial buy algorithm. Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. @@ -435,16 +434,16 @@ Best result: You should understand this result like: -- The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `'buy_adx_enabled': False`) -- You should **consider** using the RSI indicator (`'buy_rsi_enabled': True` and the best value is `29.0` (`'buy_rsi': 29.0`) +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `'buy_adx_enabled': False`. +* You should **consider** using the RSI indicator (`'buy_rsi_enabled': True`) and the best value is `29.0` (`'buy_rsi': 29.0`) -Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste it at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. +Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. Transferring your whole hyperopt result to your strategy would then look like: ```python -class MyAwsomeStrategy(IStrategy): +class MyAwesomeStrategy(IStrategy): # Buy hyperspace params: buy_params = { 'buy_adx': 44, @@ -455,13 +454,6 @@ class MyAwsomeStrategy(IStrategy): } ``` -By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. - -You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. - -!!! Note "Windows and color output" - Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. - ### Understand Hyperopt ROI results If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: @@ -588,6 +580,15 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +### Output formatting + +By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. + +You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. + +!!! Note "Windows and color output" + Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md index 8c6972b5f..03c1eb358 100644 --- a/docs/hyperopt_legacy.md +++ b/docs/hyperopt_legacy.md @@ -1,162 +1,29 @@ # Legacy Hyperopt -This page explains how to tune your strategy by finding the optimal -parameters, a process called hyperparameter optimization. The bot uses several -algorithms included in the `scikit-optimize` package to accomplish this. The -search will burn all your CPU cores, make your laptop sound like a fighter jet -and still take a long time. +This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). -In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). +!!! Warning "Deprecated / legacy mode" + Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. + Please read the [main hyperopt page](hyperopt.md) for more details. -Hyperopt requires historic data to be available, just as backtesting does. -To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. +## Prepare hyperopt file -!!! Note - Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Please update your strategy class following new documentation at [Hyperopt](hyperopt.md). - -!!! Bug -Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) - -## Install hyperopt dependencies - -Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. - -!!! Note -Since Hyperopt is a resource intensive process, running it on a Raspberry Pi is not recommended nor supported. - -### Docker - -The docker-image includes hyperopt dependencies, no further action needed. - -### Easy installation script (setup.sh) / Manual installation - -```bash -source .env/bin/activate -pip install -r requirements-hyperopt.txt -``` - -## Hyperopt command reference - - -``` -usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] - [--userdir PATH] [-s NAME] [--strategy-path PATH] - [-i TIMEFRAME] [--timerange TIMERANGE] - [--data-format-ohlcv {json,jsongz,hdf5}] - [--max-open-trades INT] - [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] - [--dry-run-wallet DRY_RUN_WALLET] [-e INT] - [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] - [--print-all] [--no-color] [--print-json] [-j JOBS] - [--random-state INT] [--min-trades INT] - [--hyperopt-loss NAME] - -optional arguments: - -h, --help show this help message and exit - -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). - --timerange TIMERANGE - Specify what timerange of data to use. - --data-format-ohlcv {json,jsongz,hdf5} - Storage format for downloaded candle (OHLCV) data. - (default: `None`). - --max-open-trades INT - Override the value of the `max_open_trades` - configuration setting. - --stake-amount STAKE_AMOUNT - Override the value of the `stake_amount` configuration - setting. - --fee FLOAT Specify fee ratio. Will be applied twice (on trade - entry and exit). - --hyperopt NAME Specify hyperopt class name which will be used by the - bot. - --hyperopt-path PATH Specify additional lookup path for Hyperopt and - Hyperopt Loss functions. - --eps, --enable-position-stacking - Allow buying the same pair multiple times (position - stacking). - --dmmp, --disable-max-market-positions - Disable applying `max_open_trades` during backtest - (same as setting `max_open_trades` to a very high - number). - --enable-protections, --enableprotections - Enable protections for backtesting.Will slow - backtesting down by a considerable amount, but will - include configured protections - --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET - Starting balance, used for backtesting / hyperopt and - dry-runs. - -e INT, --epochs INT Specify number of epochs (default: 100). - --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] - Specify which parameters to hyperopt. Space-separated - list. - --print-all Print all results, not only the best ones. - --no-color Disable colorization of hyperopt results. May be - useful if you are redirecting output to a file. - --print-json Print output in JSON format. - -j JOBS, --job-workers JOBS - The number of concurrently running jobs for - hyperoptimization (hyperopt worker processes). If -1 - (default), all CPUs are used, for -2, all CPUs but one - are used, etc. If 1 is given, no parallel computing - code is used at all. - --random-state INT Set random state to some positive integer for - reproducible hyperopt results. - --min-trades INT Set minimal desired number of trades for evaluations - in the hyperopt optimization path (default: 1). - --hyperopt-loss NAME Specify the class name of the hyperopt loss function - class (IHyperOptLoss). Different functions can - generate completely different results, since the - target for optimization is different. Built-in - Hyperopt-loss-functions are: - ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, - SharpeHyperOptLoss, SharpeHyperOptLossDaily, - SortinoHyperOptLoss, SortinoHyperOptLossDaily - -Common arguments: - -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: - 'syslog', 'journald'. See the documentation for more - details. - -V, --version show program's version number and exit - -c PATH, --config PATH - Specify configuration file (default: - `userdir/config.json` or `config.json` whichever - exists). Multiple --config options may be used. Can be - set to `-` to read config from stdin. - -d PATH, --datadir PATH - Path to directory with historical backtesting data. - --userdir PATH, --user-data-dir PATH - Path to userdata directory. - -Strategy arguments: - -s NAME, --strategy NAME - Specify strategy class name which will be used by the - bot. - --strategy-path PATH Specify additional strategy lookup path. - -``` - -## Prepare Hyperopting - -Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). - -Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. +Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. !!! Tip "About this page" -For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. -The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. +### Create a Custom Hyperopt File + +The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: ``` bash freqtrade new-hyperopt --hyperopt AwesomeHyperopt ``` -### Hyperopt checklist +### Legacy Hyperopt checklist Checklist on all tasks / possibilities in hyperopt @@ -168,7 +35,7 @@ Depending on the space you want to optimize, only some of the below are required * fill `sell_indicator_space` - for sell signal optimization !!! Note -`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. Optional in hyperopt - can also be loaded from a strategy (recommended): @@ -177,8 +44,8 @@ Optional in hyperopt - can also be loaded from a strategy (recommended): * `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy !!! Note -You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. -Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. Rarely you may also need to override: @@ -187,67 +54,7 @@ Rarely you may also need to override: * `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) -!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" -You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. - - ```python - # Have a working strategy at hand. - freqtrade new-hyperopt --hyperopt EmptyHyperopt - - freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 - ``` - -### Create a Custom Hyperopt File - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -This command will create a new hyperopt file from a template, allowing you to get started quickly. - -### Configure your Guards and Triggers - -There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: - -* Inside `indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. - -There you have two different types of indicators: 1. `guards` and 2. `triggers`. - -1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. -2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". - -!!! Hint "Guards and Triggers" -Technically, there is no difference between Guards and Triggers. -However, this guide will make this distinction to make it clear that signals should not be "sticking". -Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). - -Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if -ADX > 10*". - -If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. - -#### Sell optimization - -Similar to the buy-signal above, sell-signals can also be optimized. -Place the corresponding settings into the following methods - -* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. - -The configuration and rules are the same than for buy signals. -To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. - -#### Using timeframe as a part of the Strategy - -The Strategy class exposes the timeframe value as the `self.timeframe` attribute. -The same value is available as class-attribute `HyperoptName.timeframe`. -In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. - -## Solving a Mystery +### Defining a buy signal optimization Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys. And you also wonder should you use RSI or ADX to @@ -272,13 +79,12 @@ We will start by defining a search space: ``` Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` -and `rsi-value`) and I want you test in the range of values 20 to 40. +to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. The last -one we call `trigger` and use it to decide which buy trigger we want to use. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. -So let's write the buy strategy using these values: +So let's write the buy strategy generator using these values: ```python @staticmethod @@ -321,27 +127,20 @@ It will use the given historical data and make buys based on the buy signals gen Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). !!! Note -The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. -When you want to test an indicator that isn't used by the bot currently, remember to -add it to the `populate_indicators()` method in your strategy or hyperopt file. + The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. + When you want to test an indicator that isn't used by the bot currently, remember to + add it to the `populate_indicators()` method in your strategy or hyperopt file. -## Loss-functions +### Sell optimization -Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. +Similar to the buy-signal above, sell-signals can also be optimized. +Place the corresponding settings into the following methods -A loss function must be specified via the `--hyperopt-loss ` argument (or optionally via the configuration under the `"hyperopt_loss"` key). -This class should be in its own file within the `user_data/hyperopts/` directory. +* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. -Currently, the following loss functions are builtin: - -* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. -* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) -* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) -* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) -* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) -* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) - -Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. +The configuration and rules are the same than for buy signals. +To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. ## Execute Hyperopt @@ -362,24 +161,9 @@ Doing multiple runs (executions) with a few 1000 epochs and different random sta The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. !!! Note -Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. -Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. -You can find a list of filenames with `ls -l user_data/hyperopt_results/`. - -### Execute Hyperopt with different historical data source - -If you would like to hyperopt parameters using an alternate historical data set that -you have on-disk, use the `--datadir PATH` option. By default, hyperopt -uses data from directory `user_data/data`. - -### Running Hyperopt with a smaller test-set - -Use the `--timerange` argument to change how much of the test-set you want to use. -For example, to use one month of data, pass the following parameter to the hyperopt call: - -```bash -freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 -``` + Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. + Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. + You can find a list of filenames with `ls -l user_data/hyperopt_results/`. ### Running Hyperopt using methods from a strategy @@ -389,57 +173,6 @@ Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_t freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy ``` -### Running Hyperopt with Smaller Search Space - -Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. -Often it might make more sense to start by just searching for initial buy algorithm. -Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. - -Legal values are: - -* `all`: optimize everything -* `buy`: just search for a new buy strategy -* `sell`: just search for a new sell strategy -* `roi`: just optimize the minimal profit table for your strategy -* `stoploss`: search for the best stoploss value -* `trailing`: search for the best trailing stop values -* `default`: `all` except `trailing` -* space-separated list of any of the above values for example `--spaces roi stoploss` - -The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. - -### Position stacking and disabling max market positions - -In some situations, you may need to run Hyperopt (and Backtesting) with the -`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. - -By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one -open trade is allowed for every traded pair. The total number of trades open for all pairs -is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to -some potential trades to be hidden (or masked) by previously open trades. - -The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, -while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` -during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high -number). - -!!! Note -Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. - -You can also enable position stacking in the configuration file by explicitly setting -`"position_stacking"=true`. - -### Reproducible results - -The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. - -The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. - -If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. - -If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. - ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to create a new strategy. @@ -460,9 +193,9 @@ Buy hyperspace params: You should understand this result like: -- The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `adx-enabled: False`) -- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `adx-enabled: False`) +* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) You have to look inside your strategy file into `buy_strategy_generator()` method, what those values match to. @@ -486,63 +219,6 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: return dataframe ``` -By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. - -You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. - -!!! Note "Windows and color output" -Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. - -### Understand Hyperopt ROI results - -If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -ROI table: -{ 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0} -``` - -In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: - -``` - # Minimal ROI designed for the strategy. - # This attribute will be overridden if the config file contains "minimal_roi" - minimal_roi = { - 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0 - } -``` - -As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file. - -#### Default ROI Search Space - -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): - -| # step | 1m | | 5m | | 1h | | 1d | | -| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | -| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | -| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | -| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | -| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | - -These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. - -If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. - -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). - -A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - ### Understand Hyperopt Stoploss results If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: @@ -571,56 +247,10 @@ In order to use this best stoploss value found by Hyperopt in backtesting and fo As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. -#### Default Stoploss Search Space - -If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace vary in the range -0.35...-0.02, which is sufficient in most cases. - -If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. - -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - -### Understand Hyperopt Trailing Stop results - -If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: - -``` -Best result: - - 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 - -Trailing stop: -{ 'trailing_only_offset_is_reached': True, - 'trailing_stop': True, - 'trailing_stop_positive': 0.02001, - 'trailing_stop_positive_offset': 0.06038} -``` - -In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: - -``` python - # Trailing stop - # These attributes will be overridden if the config file contains corresponding values. - trailing_stop = True - trailing_stop_positive = 0.02001 - trailing_stop_positive_offset = 0.06038 - trailing_only_offset_is_reached = True -``` - -As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file. - -#### Default Trailing Stop Search Space - -If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. - -Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - -## Show details of Hyperopt results - -After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. ## Validate backtesting results -Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. +Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. From 32a503491d76fa84f4c4823095b279eff0515bfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 15:41:43 +0200 Subject: [PATCH 0105/1386] Reorder hyperopt methods --- docs/hyperopt.md | 70 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 725afebf3..806ce3c94 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -355,10 +355,12 @@ uses data from directory `user_data/data`. ### Running Hyperopt with a smaller test-set Use the `--timerange` argument to change how much of the test-set you want to use. -For example, to use one month of data, pass the following parameter to the hyperopt call: +For example, to use one month of data, pass `--timerange 20210101-20210201` (from january 2021 - february 2021) to the hyperopt call. + +Full command: ```bash -freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 +freqtrade hyperopt --hyperopt --strategy --timerange 20210101-20210201 ``` ### Running Hyperopt with Smaller Search Space @@ -381,37 +383,6 @@ Legal values are: The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. -### Position stacking and disabling max market positions - -In some situations, you may need to run Hyperopt (and Backtesting) with the -`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. - -By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one -open trade is allowed for every traded pair. The total number of trades open for all pairs -is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to -some potential trades to be hidden (or masked) by previously open trades. - -The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, -while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` -during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high -number). - -!!! Note - Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. - -You can also enable position stacking in the configuration file by explicitly setting -`"position_stacking"=true`. - -### Reproducible results - -The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. - -The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. - -If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. - -If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. - ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to update your strategy. @@ -580,7 +551,17 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). -### Output formatting +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. + +## Output formatting By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. @@ -589,6 +570,27 @@ You can use the `--print-all` command line option if you would like to see all r !!! Note "Windows and color output" Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. +## Position stacking and disabling max market positions + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previously open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high +number). + +!!! Note + Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. From c2d43a526cf5f0e96f016851b2b74eb814c9ac74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:08:08 +0200 Subject: [PATCH 0106/1386] Combine Legacy and advanced hyperopt sections --- docs/advanced-hyperopt.md | 349 ++++++++++++++++++++++++++++++-------- docs/hyperopt.md | 14 +- docs/hyperopt_legacy.md | 259 ---------------------------- 3 files changed, 282 insertions(+), 340 deletions(-) delete mode 100644 docs/hyperopt_legacy.md diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index bdaafb936..6a559ec96 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -4,79 +4,6 @@ This page explains some advanced Hyperopt topics that may require higher coding skills and Python knowledge than creation of an ordinal hyperoptimization class. -## Derived hyperopt classes - -Custom hyperopt classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies). - -Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace: - -```python -class MyAwesomeHyperOpt(IHyperOpt): - ... - # Uses default stoploss dimension - -class MyAwesomeHyperOpt2(MyAwesomeHyperOpt): - @staticmethod - def stoploss_space() -> List[Dimension]: - # Override boundaries for stoploss - return [ - Real(-0.33, -0.01, name='stoploss'), - ] -``` - -and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case: - -``` -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... -or -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... -``` - -## Sharing methods with your strategy - -Hyperopt classes provide access to the Strategy via the `strategy` class attribute. -This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. - -``` python -from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy -import freqtrade.vendor.qtpylib.indicators as qtpylib - -class MyAwesomeStrategy(IStrategy): - - buy_params = { - 'rsi-value': 30, - 'adx-value': 35, - } - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - return self.buy_strategy_generator(self.buy_params, dataframe, metadata) - - @staticmethod - def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe.loc[ - ( - qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & - dataframe['adx'] > params['adx-value']) & - dataframe['volume'] > 0 - ) - , 'buy'] = 1 - return dataframe - -class MyAwesomeHyperOpt(IHyperOpt): - ... - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - # Call strategy's buy strategy generator - return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) - - return populate_buy_trend -``` - ## Creating and using a custom loss function To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. @@ -142,3 +69,279 @@ This function needs to return a floating point number (`float`). Smaller numbers !!! Note Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. + +## Legacy Hyperopt + +This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). + +!!! Warning "Deprecated / legacy mode" + Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. + Please read the [main hyperopt page](hyperopt.md) for more details. + +### Prepare hyperopt file + +Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. + +!!! Tip "About this page" + For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + +#### Create a Custom Hyperopt File + +The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +#### Legacy Hyperopt checklist + +Checklist on all tasks / possibilities in hyperopt + +Depending on the space you want to optimize, only some of the below are required: + +* fill `buy_strategy_generator` - for buy signal optimization +* fill `indicator_space` - for buy signal optimization +* fill `sell_strategy_generator` - for sell signal optimization +* fill `sell_indicator_space` - for sell signal optimization + +!!! Note + `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + +Optional in hyperopt - can also be loaded from a strategy (recommended): + +* `populate_indicators` - fallback to create indicators +* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy +* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy + +!!! Note + You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) +* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) + +#### Defining a buy signal optimization + +Let's say you are curious: should you use MACD crossings or lower Bollinger +Bands to trigger your buys. And you also wonder should you use RSI or ADX to +help with those buy decisions. If you decide to use RSI or ADX, which values +should I use for them? So let's use hyperparameter optimization to solve this +mystery. + +We will start by defining a search space: + +```python + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching strategy parameters + """ + return [ + Integer(20, 40, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') + ] +``` + +Above definition says: I have five parameters I want you to randomly combine +to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. +Then we have three category variables. First two are either `True` or `False`. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. + +So let's write the buy strategy generator using these values: + +```python + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] > params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] < params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend +``` + +Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. +It will use the given historical data and make buys based on the buy signals generated with the above function. +Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). + +!!! Note + The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. + When you want to test an indicator that isn't used by the bot currently, remember to + add it to the `populate_indicators()` method in your strategy or hyperopt file. + +#### Sell optimization + +Similar to the buy-signal above, sell-signals can also be optimized. +Place the corresponding settings into the following methods + +* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. + +The configuration and rules are the same than for buy signals. +To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. + +### Execute Hyperopt + +Once you have updated your hyperopt configuration you can run it. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. + +We strongly recommend to use `screen` or `tmux` to prevent any connection loss. + +```bash +freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +``` + +Use `` as the name of the custom hyperopt used. + +The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. +Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. + +The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. + +!!! Note + Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. + Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. + You can find a list of filenames with `ls -l user_data/hyperopt_results/`. + +#### Running Hyperopt using methods from a strategy + +Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. + +```bash +freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy +``` + +### Understand the Hyperopt Result + +Once Hyperopt is completed you can use the result to create a new strategy. +Given the following result from hyperopt: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +``` + +You should understand this result like: + +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `adx-enabled: False`) +* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) + +You have to look inside your strategy file into `buy_strategy_generator()` +method, what those values match to. + +So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: + +```python +(dataframe['rsi'] < 29.0) +``` + +Translating your whole hyperopt result as the new buy-signal would then look like: + +```python +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + (dataframe['rsi'] < 29.0) & # rsi-value + dataframe['close'] < dataframe['bb_lowerband'] # trigger + ), + 'buy'] = 1 + return dataframe +``` + +### Validate backtesting results + +Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. + +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. + +Should results don't match, please double-check to make sure you transferred all conditions correctly. +Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. +You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). + +### Sharing methods with your strategy + +Hyperopt classes provide access to the Strategy via the `strategy` class attribute. +This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. + +``` python +from pandas import DataFrame +from freqtrade.strategy.interface import IStrategy +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + + buy_params = { + 'rsi-value': 30, + 'adx-value': 35, + } + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return self.buy_strategy_generator(self.buy_params, dataframe, metadata) + + @staticmethod + def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & + dataframe['adx'] > params['adx-value']) & + dataframe['volume'] > 0 + ) + , 'buy'] = 1 + return dataframe + +class MyAwesomeHyperOpt(IHyperOpt): + ... + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + # Call strategy's buy strategy generator + return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) + + return populate_buy_trend +``` diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 806ce3c94..cb8d4ad9d 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -15,7 +15,7 @@ To learn how to get data for the pairs and exchange you're interested in, head o !!! Note Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy. The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt. - The legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + The legacy documentation is available at [Legacy Hyperopt](advanced-hyperopt.md#legacy-hyperopt). ## Install hyperopt dependencies @@ -247,12 +247,11 @@ class MyAwesomeStrategy(IStrategy): buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), ``` -Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`buy_adx` -and `buy_rsi`) and I want you test in the range of values 20 to 40. +Above definition says: I have five parameters I want to randomly combine to find the best combination. +Two of them are integer values (`buy_adx` and `buy_rsi`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. The last -one we call `trigger` and use it to decide which buy trigger we want to use. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. So let's write the buy strategy using these values: @@ -349,8 +348,7 @@ The `--spaces all` option determines that all possible parameters should be opti ### Execute Hyperopt with different historical data source If you would like to hyperopt parameters using an alternate historical data set that -you have on-disk, use the `--datadir PATH` option. By default, hyperopt -uses data from directory `user_data/data`. +you have on-disk, use the `--datadir PATH` option. By default, hyperopt uses data from directory `user_data/data`. ### Running Hyperopt with a smaller test-set diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md deleted file mode 100644 index 03c1eb358..000000000 --- a/docs/hyperopt_legacy.md +++ /dev/null @@ -1,259 +0,0 @@ -# Legacy Hyperopt - -This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). - -!!! Warning "Deprecated / legacy mode" - Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. - Please read the [main hyperopt page](hyperopt.md) for more details. - -## Prepare hyperopt file - -Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. - -!!! Tip "About this page" - For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. - -### Create a Custom Hyperopt File - -The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -### Legacy Hyperopt checklist - -Checklist on all tasks / possibilities in hyperopt - -Depending on the space you want to optimize, only some of the below are required: - -* fill `buy_strategy_generator` - for buy signal optimization -* fill `indicator_space` - for buy signal optimization -* fill `sell_strategy_generator` - for sell signal optimization -* fill `sell_indicator_space` - for sell signal optimization - -!!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. - -Optional in hyperopt - can also be loaded from a strategy (recommended): - -* `populate_indicators` - fallback to create indicators -* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy -* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy - -!!! Note - You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. - Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. - -Rarely you may also need to override: - -* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) -* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) -* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) -* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) - -### Defining a buy signal optimization - -Let's say you are curious: should you use MACD crossings or lower Bollinger -Bands to trigger your buys. And you also wonder should you use RSI or ADX to -help with those buy decisions. If you decide to use RSI or ADX, which values -should I use for them? So let's use hyperparameter optimization to solve this -mystery. - -We will start by defining a search space: - -```python - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching strategy parameters - """ - return [ - Integer(20, 40, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') - ] -``` - -Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. -Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. -The last one we call `trigger` and use it to decide which buy trigger we want to use. - -So let's write the buy strategy generator using these values: - -```python - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) - - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) - - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend -``` - -Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. -It will use the given historical data and make buys based on the buy signals generated with the above function. -Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). - -!!! Note - The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. - When you want to test an indicator that isn't used by the bot currently, remember to - add it to the `populate_indicators()` method in your strategy or hyperopt file. - -### Sell optimization - -Similar to the buy-signal above, sell-signals can also be optimized. -Place the corresponding settings into the following methods - -* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. - -The configuration and rules are the same than for buy signals. -To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. - -## Execute Hyperopt - -Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. - -We strongly recommend to use `screen` or `tmux` to prevent any connection loss. - -```bash -freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all -``` - -Use `` as the name of the custom hyperopt used. - -The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. -Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. - -The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. - -!!! Note - Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. - Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. - You can find a list of filenames with `ls -l user_data/hyperopt_results/`. - -### Running Hyperopt using methods from a strategy - -Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. - -```bash -freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy -``` - -## Understand the Hyperopt Result - -Once Hyperopt is completed you can use the result to create a new strategy. -Given the following result from hyperopt: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -``` - -You should understand this result like: - -* The buy trigger that worked best was `bb_lower`. -* You should not use ADX because `adx-enabled: False`) -* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) - -You have to look inside your strategy file into `buy_strategy_generator()` -method, what those values match to. - -So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: - -```python -(dataframe['rsi'] < 29.0) -``` - -Translating your whole hyperopt result as the new buy-signal would then look like: - -```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - dataframe.loc[ - ( - (dataframe['rsi'] < 29.0) & # rsi-value - dataframe['close'] < dataframe['bb_lowerband'] # trigger - ), - 'buy'] = 1 - return dataframe -``` - -### Understand Hyperopt Stoploss results - -If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -Stoploss: -0.27996 -``` - -In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: - -``` python - # Optimal stoploss designed for the strategy - # This attribute will be overridden if the config file contains "stoploss" - stoploss = -0.27996 -``` - -As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. - - -## Validate backtesting results - -Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. - -To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. - -Should results don't match, please double-check to make sure you transferred all conditions correctly. -Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. -You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). From 093d6ce8af24bdf37a14b0505f23dd414d40682f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:13:49 +0200 Subject: [PATCH 0107/1386] Add sample for Nested space --- docs/advanced-hyperopt.md | 12 ++++++++++++ docs/hyperopt.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 6a559ec96..723163b2c 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -70,6 +70,18 @@ This function needs to return a floating point number (`float`). Smaller numbers !!! Note Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. +## Overriding pre-defined spaces + +To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows: + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + # Define a custom stoploss space. + def stoploss_space(self): + return [Real(-0.05, -0.01, name='stoploss')] +``` + ## Legacy Hyperopt This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). diff --git a/docs/hyperopt.md b/docs/hyperopt.md index cb8d4ad9d..1ce1f9a86 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -151,7 +151,7 @@ Depending on the space you want to optimize, only some of the below are required !!! Note `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. -Rarely you may also need to create a nested class named `HyperOpt` and implement: +Rarely you may also need to create a [nested class](advanced-hyperopt.md#overriding-pre-defined-spaces) named `HyperOpt` and implement * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) From 771fc057494a55902624e633d9ff8066eb1da323 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:32:16 +0200 Subject: [PATCH 0108/1386] Update sample strategy with hyperoptable Parameters --- freqtrade/templates/base_strategy.py.j2 | 4 +++- freqtrade/templates/sample_strategy.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index dd6b773e1..9d69ee520 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 # --- Do not remove these libs --- import numpy as np # noqa @@ -6,6 +7,7 @@ import pandas as pd # noqa from pandas import DataFrame from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter # -------------------------------- # Add your lib to import here @@ -16,7 +18,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class {{ strategy }}(IStrategy): """ This is a strategy template to get you started. - More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + More information in https://www.freqtrade.io/en/latest/strategy-customization/ You can: :return: a Dataframe with all mandatory indicators for the strategies diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index db1ba48b8..84f3fbc9e 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -1,11 +1,13 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 # isort: skip_file # --- Do not remove these libs --- import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter # -------------------------------- # Add your lib to import here @@ -52,7 +54,11 @@ class SampleStrategy(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Optimal ticker interval for the strategy. + # Hyperoptable parameters + buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) + sell_rsi = IntParameter(low=50, high=100, defualt=70, space='buy', optimize=True, load=True) + + # Optimal timeframe for the strategy. timeframe = '5m' # Run "populate_indicators()" only for new candle. @@ -339,7 +345,8 @@ class SampleStrategy(IStrategy): """ dataframe.loc[ ( - (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + # Signal: RSI crosses above 30 + (qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['volume'] > 0) # Make sure Volume is not 0 @@ -357,7 +364,8 @@ class SampleStrategy(IStrategy): """ dataframe.loc[ ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 + # Signal: RSI crosses above 70 + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['volume'] > 0) # Make sure Volume is not 0 From 6555454bd287a230de64e9686a09f6bf70fd36fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:54:47 +0200 Subject: [PATCH 0109/1386] Remove more ticker_interval occurances --- docs/backtesting.md | 3 +-- docs/edge.md | 3 +-- docs/hyperopt.md | 3 +-- docs/plotting.md | 6 ++---- docs/utils.md | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/commands/list_commands.py | 2 +- freqtrade/optimize/hyperopt_interface.py | 6 +++--- freqtrade/templates/sample_strategy.py | 2 +- tests/optimize/test_backtest_detail.py | 2 +- tests/strategy/strats/default_strategy.py | 2 +- tests/strategy/strats/legacy_strategy.py | 2 +- 12 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index d02c59f05..c8acfdbe1 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -23,8 +23,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --data-format-ohlcv {json,jsongz,hdf5} diff --git a/docs/edge.md b/docs/edge.md index 0aa76cd12..7f0a9cb2d 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -221,8 +221,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --max-open-trades INT diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 96c7354b9..7ae06660b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -53,8 +53,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --data-format-ohlcv {json,jsongz,hdf5} diff --git a/docs/plotting.md b/docs/plotting.md index d7ed5ab1f..63afa16b6 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -66,8 +66,7 @@ optional arguments: --timerange TIMERANGE Specify what timerange of data to use. -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --no-trades Skip using trades from backtesting file and DB. Common arguments: @@ -264,8 +263,7 @@ optional arguments: Specify the source for trades (Can be DB or file (backtest file)) Default: file -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/utils.md b/docs/utils.md index cf7d5f1d1..a84f068e9 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -264,7 +264,7 @@ All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpr ## List Timeframes -Use the `list-timeframes` subcommand to see the list of timeframes (ticker intervals) available for the exchange. +Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange. ``` usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 15c13cec9..12c03d824 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -118,7 +118,7 @@ AVAILABLE_CLI_OPTIONS = { # Optimize common "timeframe": Arg( '-i', '--timeframe', '--ticker-interval', - help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', + help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).', ), "timerange": Arg( '--timerange', diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 5f53fc824..d509bfaa5 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -99,7 +99,7 @@ def start_list_hyperopts(args: Dict[str, Any]) -> None: def start_list_timeframes(args: Dict[str, Any]) -> None: """ - Print ticker intervals (timeframes) available on Exchange + Print timeframes available on Exchange """ config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Do not use timeframe set in the config diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..a9bbc021c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -31,7 +31,7 @@ class IHyperOpt(ABC): Defines the mandatory structure must follow any custom hyperopt Class attributes you can use: - ticker_interval -> int: value of the ticker interval to use for the strategy + timeframe -> int: value of the timeframe to use for the strategy """ ticker_interval: str # DEPRECATED timeframe: str @@ -97,7 +97,7 @@ class IHyperOpt(ABC): This method implements adaptive roi hyperspace with varied ranges for parameters which automatically adapts to the - ticker interval used. + timeframe used. It's used by Freqtrade by default, if no custom roi_space method is defined. """ @@ -119,7 +119,7 @@ class IHyperOpt(ABC): # * 'roi_p' (limits for the ROI value steps) components are scaled logarithmically. # # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() - # method for the 5m ticker interval. + # method for the 5m timeframe. roi_t_scale = timeframe_min / 5 roi_p_scale = math.log1p(timeframe_min) / math.log1p(5) roi_limits = { diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 5dfa42bcc..904597d21 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -53,7 +53,7 @@ class SampleStrategy(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Optimal ticker interval for the strategy. + # Optimal timeframe for the strategy. timeframe = '5m' # Run "populate_indicators()" only for new candle. diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 0ba6f4a7f..3655b941d 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -268,7 +268,7 @@ tc16 = BTContainer(data=[ # Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 # Causes negative profit even though sell-reason is ROI. # stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) -# Uses open as sell-rate (special case) - since the roi-time is a multiple of the ticker interval. +# Uses open as sell-rate (special case) - since the roi-time is a multiple of the timeframe. tc17 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], diff --git a/tests/strategy/strats/default_strategy.py b/tests/strategy/strats/default_strategy.py index 98842ff7c..7171b93ae 100644 --- a/tests/strategy/strats/default_strategy.py +++ b/tests/strategy/strats/default_strategy.py @@ -28,7 +28,7 @@ class DefaultStrategy(IStrategy): # Optimal stoploss designed for the strategy stoploss = -0.10 - # Optimal ticker interval for the strategy + # Optimal timeframe for the strategy timeframe = '5m' # Optional order type mapping diff --git a/tests/strategy/strats/legacy_strategy.py b/tests/strategy/strats/legacy_strategy.py index 1e7bb5e1e..9ef00b110 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -31,7 +31,7 @@ class TestStrategyLegacy(IStrategy): # This attribute will be overridden if the config file contains "stoploss" stoploss = -0.10 - # Optimal ticker interval for the strategy + # Optimal timeframe for the strategy # Keep the legacy value here to test compatibility ticker_interval = '5m' From 5f6eae52a296a83b6426bb6c8f0104bc59577c83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 19:12:36 +0200 Subject: [PATCH 0110/1386] fix too long performance message closes #4655 --- freqtrade/rpc/telegram.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 92899d67f..b418c3dab 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -695,14 +695,18 @@ class Telegram(RPCHandler): """ try: trades = self._rpc._rpc_performance() - stats = '\n'.join('{index}.\t{pair}\t{profit:.2f}% ({count})'.format( - index=i + 1, - pair=trade['pair'], - profit=trade['profit'], - count=trade['count'] - ) for i, trade in enumerate(trades)) - message = 'Performance:\n{}'.format(stats) - self._send_msg(message, parse_mode=ParseMode.HTML) + output = "Performance:\n" + for i, trade in enumerate(trades): + stat_line = (f"{i+1}.\t {trade['pair']}\t{trade['profit']:.2f}% " + f"({trade['count']})\n") + + if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + self._send_msg(output) + output = stat_line + else: + output += stat_line + + self._send_msg(output, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) From 9d4b5cc6bb961125df6a5afa94ed62848537f010 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 17:10:39 +0200 Subject: [PATCH 0111/1386] Fix typo --- freqtrade/optimize/hyperopt_interface.py | 2 +- freqtrade/templates/sample_strategy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 8eefff99c..633c8bdd5 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -103,7 +103,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_min = timeframe_to_minutes(self.ticker_interval) + timeframe_min = timeframe_to_minutes(self.timeframe) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 29b550ea4..a51b30f3f 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -57,7 +57,7 @@ class SampleStrategy(IStrategy): # Hyperoptable parameters buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) - sell_rsi = IntParameter(low=50, high=100, defualt=70, space='buy', optimize=True, load=True) + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) # Optimal timeframe for the strategy. timeframe = '5m' From 30e5e9296817f54ca773772a124548fd290c9d9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 20:17:48 +0200 Subject: [PATCH 0112/1386] Don't allow one parmeter to be in 2 spaces use the explicit user wish (given explicitly with "space") --- freqtrade/strategy/hyper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 6282d91c0..e7f31e20d 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -244,8 +244,8 @@ class HyperStrategyMixin(object): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): - if category is None or category == attr.category or \ - attr_name.startswith(category + '_'): + if (category is None or category == attr.category or + (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr def _load_params(self, params: dict) -> None: From 9e56f6d4ebba51b07b6facb81da588306741d488 Mon Sep 17 00:00:00 2001 From: rextea Date: Sun, 4 Apr 2021 01:19:38 +0300 Subject: [PATCH 0113/1386] Sort pair lists by total profit --- freqtrade/optimize/optimize_reports.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 099976aa9..a80dc5d31 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -110,6 +110,9 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b tabular_data.append(_generate_result_line(result, starting_balance, pair)) + # Sort by total profit %: + tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) + # Append Total tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data From c2be9b971c89795adcbdc98bd2aee2ae8a039c5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Apr 2021 07:02:59 +0200 Subject: [PATCH 0114/1386] Improve backtest assumptions with fill rules --- docs/backtesting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index c8acfdbe1..e16225f94 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -420,6 +420,7 @@ It contains some useful key metrics about performance of your strategy on backte Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: - Buys happen at open-price +- All orders are filled at the requested price (no slippage, no unfilled orders) - Sell-signal sells happen at open-price of the consecutive candle - Sell-signal is favored over Stoploss, because sell-signals are assumed to trigger on candle's open - ROI From 342f14472c1e6fb4082218424d865cb1c97675d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:26:45 +0000 Subject: [PATCH 0115/1386] Bump mkdocs-material from 7.0.7 to 7.1.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.7 to 7.1.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.0.7...7.1.0) 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 711b6ca46..3dbaea111 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.7 +mkdocs-material==7.1.0 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From fc2f9fd0c77cf0791952e42f39cd25e54f042cbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:16 +0000 Subject: [PATCH 0116/1386] Bump ccxt from 1.45.44 to 1.46.38 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.45.44 to 1.46.38. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.45.44...1.46.38) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4984cf47..5177ae5d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.45.44 +ccxt==1.46.38 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From 320172a224172582a0156cda6ccd7e4f0fad69c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:26 +0000 Subject: [PATCH 0117/1386] Bump sqlalchemy from 1.4.3 to 1.4.5 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.3 to 1.4.5. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4984cf47..9b3638789 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.45.44 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.3 +SQLAlchemy==1.4.5 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 36b39f91365400f011410f0ac2830b456040b1bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:31 +0000 Subject: [PATCH 0118/1386] Bump pycoingecko from 1.4.0 to 1.4.1 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/1.4.0...1.4.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4984cf47..964701830 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonschema==3.2.0 TA-Lib==0.4.19 technical==1.2.2 tabulate==0.8.9 -pycoingecko==1.4.0 +pycoingecko==1.4.1 jinja2==2.11.3 tables==3.6.1 blosc==1.10.2 From abbc56c1ccae81cd32e013f698f1eb60b601deaa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:44 +0000 Subject: [PATCH 0119/1386] Bump pytest from 6.2.2 to 6.2.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.2 to 6.2.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.2...6.2.3) 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 02f7fbca8..cd93f2433 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.9.0 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 -pytest==6.2.2 +pytest==6.2.3 pytest-asyncio==0.14.0 pytest-cov==2.11.1 pytest-mock==3.5.1 From 0407bf755f157cf812b718faea5ac8338ae9787d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 07:28:51 +0200 Subject: [PATCH 0120/1386] Use .query.session to make sure the scoped session is used properly --- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/persistence/models.py | 21 +++++++++----------- freqtrade/persistence/pairlock_middleware.py | 6 +++--- freqtrade/rpc/rpc.py | 6 +++--- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dd6966848..a701e8db9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -187,7 +187,7 @@ class FreqtradeBot(LoggingMixin): if self.get_free_open_trades(): self.enter_positions() - Trade.session.flush() + Trade.query.session.flush() def process_stopped(self) -> None: """ @@ -621,8 +621,8 @@ class FreqtradeBot(LoggingMixin): if order_status == 'closed': self.update_trade_state(trade, order_id, order) - Trade.session.add(trade) - Trade.session.flush() + Trade.query.session.add(trade) + Trade.query.session.flush() # Updating wallets self.wallets.update() @@ -1205,7 +1205,7 @@ class FreqtradeBot(LoggingMixin): # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) - Trade.session.flush() + Trade.query.session.flush() # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 465f3d443..a82c047c3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -61,11 +61,8 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # We should use the scoped_session object - not a seperately initialized version Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) Trade.query = Trade.session.query_property() - # Copy session attributes to order object too - Order.session = Trade.session - Order.query = Order.session.query_property() - PairLock.session = Trade.session - PairLock.query = PairLock.session.query_property() + Order.query = Trade.session.query_property() + PairLock.query = Trade.session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) @@ -81,7 +78,7 @@ def cleanup_db() -> None: Flushes all pending operations to disk. :return: None """ - Trade.session.flush() + Trade.query.session.flush() def clean_dry_run_db() -> None: @@ -677,7 +674,7 @@ class LocalTrade(): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade.session.query( + total_open_stake_amount = Trade.query.with_entities( func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() else: total_open_stake_amount = sum( @@ -689,7 +686,7 @@ class LocalTrade(): """ Returns List of dicts containing all Trades, including profit and trade count """ - pair_rates = Trade.session.query( + pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), func.count(Trade.pair).label('count') @@ -712,7 +709,7 @@ class LocalTrade(): Get best pair with closed trade. :returns: Tuple containing (pair, profit_sum) """ - best_pair = Trade.session.query( + best_pair = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum') ).filter(Trade.is_open.is_(False)) \ .group_by(Trade.pair) \ @@ -805,10 +802,10 @@ class Trade(_DECL_BASE, LocalTrade): def delete(self) -> None: for order in self.orders: - Order.session.delete(order) + Order.query.session.delete(order) - Trade.session.delete(self) - Trade.session.flush() + Trade.query.session.delete(self) + Trade.query.session.flush() @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index f0048bb52..245f7cdab 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -48,8 +48,8 @@ class PairLocks(): active=True ) if PairLocks.use_db: - PairLock.session.add(lock) - PairLock.session.flush() + PairLock.query.session.add(lock) + PairLock.query.session.flush() else: PairLocks.locks.append(lock) @@ -99,7 +99,7 @@ class PairLocks(): for lock in locks: lock.active = False if PairLocks.use_db: - PairLock.session.flush() + PairLock.query.session.flush() @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1359729b9..59758a573 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -558,7 +558,7 @@ class RPC: # Execute sell for all open orders for trade in Trade.get_open_trades(): _exec_forcesell(trade) - Trade.session.flush() + Trade.query.session.flush() self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} @@ -571,7 +571,7 @@ class RPC: raise RPCException('invalid argument') _exec_forcesell(trade) - Trade.session.flush() + Trade.query.session.flush() self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} @@ -696,7 +696,7 @@ class RPC: lock.lock_end_time = datetime.now(timezone.utc) # session is always the same - PairLock.session.flush() + PairLock.query.session.flush() return self._rpc_locks() From ea0b47a7f9c1625e6ec651d656688e19bff41781 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 07:38:07 +0200 Subject: [PATCH 0121/1386] Replace test occurances of Trade.session with Trade.query.session --- tests/conftest.py | 2 +- tests/plugins/test_protections.py | 42 +++++++++++++++---------------- tests/rpc/test_rpc_apiserver.py | 12 ++++----- tests/test_freqtradebot.py | 35 +++++++++++--------------- tests/test_integration.py | 1 - tests/test_persistence.py | 8 +++--- 6 files changed, 46 insertions(+), 54 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3522ef02d..f8c1c5357 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -197,7 +197,7 @@ def create_mock_trades(fee, use_db: bool = True): """ def add_trade(trade): if use_db: - Trade.session.add(trade) + Trade.query.session.add(trade) else: LocalTrade.add_bt_trade(trade) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2e42c1be4..545387eaa 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -91,7 +91,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, )) @@ -100,12 +100,12 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() # This trade does not count, as it's closed too long ago - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'BCH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=250, min_ago_close=100, )) - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=240, min_ago_close=30, )) @@ -114,7 +114,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'LTC/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=180, min_ago_close=30, )) @@ -148,7 +148,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, profit_rate=0.9, )) @@ -158,12 +158,12 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair assert not log_has_re(message, caplog) caplog.clear() # This trade does not count, as it's closed too long ago - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=250, min_ago_close=100, profit_rate=0.9, )) # Trade does not count for per pair stop as it's the wrong pair. - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=240, min_ago_close=30, profit_rate=0.9, )) @@ -178,7 +178,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair caplog.clear() # 2nd Trade that counts with correct pair - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=180, min_ago_close=30, profit_rate=0.9, )) @@ -203,7 +203,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, )) @@ -213,7 +213,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=205, min_ago_close=35, )) @@ -242,7 +242,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=800, min_ago_close=450, profit_rate=0.9, )) @@ -253,7 +253,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=120, profit_rate=0.9, )) @@ -265,14 +265,14 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() # Add positive trade - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=20, min_ago_close=10, profit_rate=1.15, )) assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not PairLocks.is_pair_locked('XRP/BTC') - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=110, min_ago_close=20, profit_rate=0.8, )) @@ -300,15 +300,15 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not freqtrade.protections.stop_per_pair('XRP/BTC') caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'NEO/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) @@ -316,7 +316,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not freqtrade.protections.global_stop() assert not freqtrade.protections.stop_per_pair('XRP/BTC') - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=500, min_ago_close=400, profit_rate=0.9, )) @@ -326,7 +326,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, )) @@ -339,7 +339,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) # Winning trade ... (should not lock, does not change drawdown!) - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=320, min_ago_close=410, profit_rate=1.5, )) @@ -349,7 +349,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): caplog.clear() # Add additional negative trade, causing a loss of > 15% - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=20, min_ago_close=10, profit_rate=0.8, )) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 20d32024f..180eefa08 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -510,7 +510,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 0 create_mock_trades(fee) - Trade.session.flush() + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) @@ -538,7 +538,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets): assert_response(rc, 502) create_mock_trades(fee) - Trade.session.flush() + Trade.query.session.flush() ftbot.strategy.order_types['stoploss_on_exchange'] = True trades = Trade.query.all() trades[1].stoploss_order_id = '1234' @@ -720,7 +720,7 @@ def test_api_performance(botclient, mocker, ticker, fee): ) trade.close_profit = trade.calc_profit_ratio() - Trade.session.add(trade) + Trade.query.session.add(trade) trade = Trade( pair='XRP/ETH', @@ -735,8 +735,8 @@ def test_api_performance(botclient, mocker, ticker, fee): close_rate=0.391 ) trade.close_profit = trade.calc_profit_ratio() - Trade.session.add(trade) - Trade.session.flush() + Trade.query.session.add(trade) + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) @@ -764,7 +764,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): trades = Trade.get_open_trades() trades[0].open_order_id = None ftbot.exit_positions(trades) - Trade.session.flush() + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 486c31090..c93f8b858 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -768,7 +768,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, assert pair not in default_conf['exchange']['pair_whitelist'] # create open trade not in whitelist - Trade.session.add(Trade( + Trade.query.session.add(Trade( pair=pair, stake_amount=0.001, fee_open=fee.return_value, @@ -778,7 +778,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, open_rate=0.01, exchange='bittrex', )) - Trade.session.add(Trade( + Trade.query.session.add(Trade( pair='ETH/BTC', stake_amount=0.001, fee_open=fee.return_value, @@ -1779,7 +1779,6 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) - Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) freqtrade = get_patched_freqtradebot(mocker, default_conf) trade = Trade( @@ -1805,7 +1804,6 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) - Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) freqtrade = get_patched_freqtradebot(mocker, default_conf) trade = Trade( @@ -1868,7 +1866,6 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) patch_exchange(mocker) - Trade.session = MagicMock() amount = limit_sell_order["amount"] freqtrade = get_patched_freqtradebot(mocker, default_conf) wallet_mock.reset_mock() @@ -2110,7 +2107,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # Ensure default is to return empty (so not mocked yet) freqtrade.check_handle_timedout() @@ -2161,7 +2158,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) # check it does cancel buy orders over the time limit @@ -2191,7 +2188,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() @@ -2218,7 +2215,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() @@ -2248,7 +2245,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ open_trade.close_profit_abs = 0.001 open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # Ensure default is false freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 @@ -2296,7 +2293,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, open_trade.close_profit_abs = 0.001 open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) # check it does cancel sell orders over the time limit @@ -2327,7 +2324,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() @@ -2353,7 +2350,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit # note this is for a partially-complete buy order @@ -2386,7 +2383,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap open_trade.fee_open = fee() open_trade.fee_close = fee() - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # cancelling a half-filled order should update the amount to the bought amount # and apply fees if necessary. freqtrade.check_handle_timedout() @@ -2426,7 +2423,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, open_trade.fee_open = fee() open_trade.fee_close = fee() - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # cancelling a half-filled order should update the amount to the bought amount # and apply fees if necessary. freqtrade.check_handle_timedout() @@ -2463,7 +2460,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.check_handle_timedout() assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, " @@ -2486,7 +2483,6 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non freqtrade = FreqtradeBot(default_conf) freqtrade._notify_buy_cancel = MagicMock() - Trade.session = MagicMock() trade = MagicMock() trade.pair = 'LTC/ETH' limit_buy_order['filled'] = 0.0 @@ -2520,7 +2516,6 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel') freqtrade = FreqtradeBot(default_conf) - Trade.session = MagicMock() reason = CANCEL_REASON['TIMEOUT'] trade = MagicMock() trade.pair = 'LTC/ETH' @@ -2549,7 +2544,6 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, freqtrade = FreqtradeBot(default_conf) freqtrade._notify_buy_cancel = MagicMock() - Trade.session = MagicMock() trade = MagicMock() trade.pair = 'LTC/ETH' limit_buy_order['filled'] = 0.0 @@ -2812,7 +2806,6 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c freqtrade.enter_positions() trade = Trade.query.first() - Trade.session = MagicMock() PairLock.session = MagicMock() freqtrade.config['dry_run'] = False @@ -4422,7 +4415,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): open_rate=0.01, exchange='bittrex', ) - Trade.session.add(trade) + Trade.query.session.add(trade) freqtrade.reupdate_buy_order_fees(trade) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) diff --git a/tests/test_integration.py b/tests/test_integration.py index 8e3bd251a..1c60faa7b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -89,7 +89,6 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, freqtrade.strategy.confirm_trade_entry.reset_mock() assert freqtrade.strategy.confirm_trade_exit.call_count == 0 wallets_mock.reset_mock() - Trade.session = MagicMock() trades = Trade.query.all() # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6a388327c..d53386287 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -403,7 +403,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='dry_run_buy_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) trade = Trade( pair='ETC/BTC', @@ -415,7 +415,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='dry_run_sell_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) # Simulate prod entry trade = Trade( @@ -428,7 +428,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='prod_buy_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) # We have 3 entries: 2 dry_run, 1 prod assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3 @@ -933,7 +933,7 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade.stop_loss_pct == -0.05 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 - Trade.session.add(trade) + Trade.query.session.add(trade) # Lower stoploss Trade.stoploss_reinitialization(0.06) From e979f132e3933effcfd2e2fb0d742b1c559883a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:57:56 +0000 Subject: [PATCH 0122/1386] Bump python from 3.9.2-slim-buster to 3.9.3-slim-buster Bumps python from 3.9.2-slim-buster to 3.9.3-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b399174b..1f6e36b68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.2-slim-buster as base +FROM python:3.9.3-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index eecd9fdc0..5e6763d4c 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.9-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.3-slim-buster as base # Setup env ENV LANG C.UTF-8 From af525818136efd4848a5b6e6e6d6c70653310d53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 08:22:01 +0200 Subject: [PATCH 0123/1386] Update Dockerfile.armhf --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 5e6763d4c..dcb008cb9 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.3-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 From 7132aefd608ab8495932f438bc66695d18d4d70f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 08:46:12 +0200 Subject: [PATCH 0124/1386] Rename Trade.session to Trade._session --- freqtrade/persistence/models.py | 8 ++++---- tests/test_persistence.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a82c047c3..a22e75e1e 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -59,10 +59,10 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version - Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) - Trade.query = Trade.session.query_property() - Order.query = Trade.session.query_property() - PairLock.query = Trade.session.query_property() + Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) + Trade.query = Trade._session.query_property() + Order.query = Trade._session.query_property() + PairLock.query = Trade._session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d53386287..3336e4e66 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -18,8 +18,8 @@ from tests.conftest import create_mock_trades, log_has, log_has_re def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url'], default_conf['dry_run']) - assert hasattr(Trade, 'session') - assert 'scoped_session' in type(Trade.session).__name__ + assert hasattr(Trade, '_session') + assert 'scoped_session' in type(Trade._session).__name__ def test_init_custom_db_url(default_conf, tmpdir): From dc406fe19f85197d9223342c7777a466caabb480 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 10:53:00 +0200 Subject: [PATCH 0125/1386] Fail in case of name and explicit space name collisions --- freqtrade/strategy/hyper.py | 4 ++++ tests/optimize/test_hyperopt.py | 2 -- tests/strategy/test_interface.py | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e7f31e20d..0b7055f01 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -244,6 +244,10 @@ class HyperStrategyMixin(object): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): + if (category and 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 is None or category == attr.category or (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 36b6f1229..c13da0d76 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1093,8 +1093,6 @@ def test_print_epoch_details(capsys): def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: - # mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - # mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed del hyperopt_conf['hyperopt'] diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 71f877cc3..3bfa691b4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -617,3 +617,8 @@ def test_auto_hyperopt_interface(default_conf): # Parameter is disabled - so value from sell_param dict will NOT be used. assert strategy.sell_minusdi.value == 0.5 + + strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy') + + with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): + [x for x in strategy.enumerate_parameters('sell')] From c51839dc3be4c47a32394719eb3bfba0e94ff1a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 11:21:20 +0200 Subject: [PATCH 0126/1386] Make the logmessage for loaded parameters clearer --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0b7055f01..709179997 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -263,7 +263,7 @@ class HyperStrategyMixin(object): if attr_name in params: if attr.load: attr.value = params[attr_name] - logger.info(f'{attr_name} = {attr.value}') + logger.info(f'Strategy Parameter: {attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') From 3044aa18e60bb5d42d880d67b27a7bea0f937653 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 14:45:42 +0200 Subject: [PATCH 0127/1386] Add warning for hyperopt-parameters --- docs/hyperopt.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index db7a23f02..07cc963cf 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -305,6 +305,9 @@ There are four parameter types each suited for different purposes. * `optimize` - when set to `False` parameter will not be included in optimization process. Use these parameters to quickly prototype various ideas. +!!! Warning + Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. From 7b2a0d46cb19cb4c219d33e4ec1fc9cfac887a40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 15:38:25 +0200 Subject: [PATCH 0128/1386] Fix typo --- freqtrade/optimize/hyperopt_auto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index ed6f2d6f7..c4d6f1581 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -33,14 +33,14 @@ class HyperOptAuto(IHyperOpt): return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - def populate_buy_trend(dataframe: DataFrame, metadata: dict): + def populate_sell_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: # noinspection PyProtectedMember attr._set_value(params[attr_name]) return self.strategy.populate_sell_trend(dataframe, metadata) - return populate_buy_trend + return populate_sell_trend def _get_func(self, name) -> Callable: """ From 78a84f8081f8cd58f0ab7a74f2f40c9106dd615a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 15:38:33 +0200 Subject: [PATCH 0129/1386] Allow --hyperoptloss in addition to --hyperopt-loss --- freqtrade/commands/cli_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index cea353109..4fac8ac72 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -267,7 +267,7 @@ AVAILABLE_CLI_OPTIONS = { default=1, ), "hyperopt_loss": Arg( - '--hyperopt-loss', + '--hyperopt-loss', '--hyperoptloss', help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' From c176e277f117badcbe495750707af79e07717b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:31:34 +0200 Subject: [PATCH 0130/1386] Add a REST endpoint for getting a specific trade --- freqtrade/rpc/api_server/api_v1.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index b983402e9..6873c0c4c 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -88,6 +88,11 @@ def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) +@router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) +def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_trade_status([tradeid])[0] + + @router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete(tradeid) From ddba0d688e0b168054fda58b0267810ff13fad98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:32:55 +0200 Subject: [PATCH 0131/1386] Add new trade endpoint to docs --- docs/rest-api.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index c41c3f24c..4e784b6af 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -125,6 +125,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `reload_config` | Reloads the configuration file. | `trades` | List last trades. +| `trade/` | Get specific trade. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. | `logs` | Shows last log messages. @@ -275,6 +276,10 @@ trades :param limit: Limits trades to the X last trades. No limit to get all the trades. +trade + Return specific trade. + :param tradeid: Specify which trade to get. + version Return the version of the bot. From fc78246bbc2a7f4260e1bf2bdd3d898c52c98110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:34:01 +0200 Subject: [PATCH 0132/1386] Some changes to rest-api docs --- docs/rest-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 4e784b6af..be3107fcb 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -182,7 +182,7 @@ count Return the amount of open trades. daily - Return the amount of open trades. + Return the profits for each day, and amount of trades. delete_lock Delete (disable) lock from the database. @@ -215,7 +215,7 @@ locks logs Show latest logs. - :param limit: Limits log messages to the last logs. No limit to get all the trades. + :param limit: Limits log messages to the last logs. No limit to get the entire log. pair_candles Return live dataframe for . From 6633752fcb743f693105c9c56633c82817593592 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 05:29:34 +0000 Subject: [PATCH 0133/1386] Bump python from 3.9.3-slim-buster to 3.9.4-slim-buster Bumps python from 3.9.3-slim-buster to 3.9.4-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1f6e36b68..711f27990 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.3-slim-buster as base +FROM python:3.9.4-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index dcb008cb9..b019b6096 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.4-slim-buster as base # Setup env ENV LANG C.UTF-8 From 0550f261f1f48626c112ef830485ac5a843f9548 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:47:44 +0200 Subject: [PATCH 0134/1386] Add exchange_has validation --- freqtrade/commands/list_commands.py | 14 +++++++++----- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/common.py | 23 +++++++++++++++++++++++ freqtrade/exchange/exchange.py | 29 ++++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index d509bfaa5..fa4bc1066 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -13,7 +13,7 @@ from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException -from freqtrade.exchange import available_exchanges, ccxt_exchanges, market_is_active +from freqtrade.exchange import market_is_active, validate_exchanges from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -28,14 +28,18 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: :param args: Cli args from Arguments() :return: None """ - exchanges = ccxt_exchanges() if args['list_exchanges_all'] else available_exchanges() + exchanges = validate_exchanges(args['list_exchanges_all']) + if args['print_one_column']: - print('\n'.join(exchanges)) + print('\n'.join([e[0] for e in exchanges])) else: if args['list_exchanges_all']: - print(f"All exchanges supported by the ccxt library: {', '.join(exchanges)}") + print("All exchanges supported by the ccxt library:") else: - print(f"Exchanges available for Freqtrade: {', '.join(exchanges)}") + print("Exchanges available for Freqtrade:") + exchanges = [e for e in exchanges if e[1] is not False] + + print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason'])) def _print_objs_tabular(objs: List, print_colorized: bool) -> None: diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 15ba7b9f6..0eedd25d8 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -12,6 +12,6 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds) + timeframe_to_seconds, validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index be0a1e483..90b70d67e 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -98,6 +98,29 @@ MAP_EXCHANGE_CHILDCLASS = { } +EXCHANGE_HAS_REQUIRED = [ + # Required / private + 'fetchOrder', + 'cancelOrder', + 'createOrder', + # 'createLimitOrder', 'createMarketOrder', + 'fetchBalance', + + # Public endpoints + 'loadMarkets', + 'fetchOHLCV', +] + +EXCHANGE_HAS_OPTIONAL = [ + # Private + 'fetchMyTrades', # Trades for order - fee detection + # Public + 'fetchOrderBook', 'fetchL2OrderBook', 'fetchTicker', # OR for pricing + 'fetchTickers', # For volumepairlist? + 'fetchTrades', # Downloading trades data +] + + def calculate_backoff(retrycount, max_retries): """ Calculate backoff diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 85c5b4c6d..768a02aa1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -23,7 +23,8 @@ from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier, +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, + EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier, retrier_async) from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -1337,6 +1338,32 @@ def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: return [x for x in exchanges if not is_exchange_bad(x)] +def validate_exchange(exchange: str) -> Tuple[bool, str]: + ex_mod = getattr(ccxt, exchange.lower())() + if not ex_mod or not ex_mod.has: + return False, '' + missing = [k for k in EXCHANGE_HAS_REQUIRED if not ex_mod.has.get(k)] + if missing: + return False, f"missing: {', '.join(missing)}" + + missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)] + if missing_opt: + return True, f"missing opt: {', '.join(missing_opt)}" + + return True, '' + + +def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]: + """ + :return: List of tuples with exchangename, valid, reason. + """ + exchanges = ccxt_exchanges() if all_exchanges else available_exchanges() + exchanges_valid = [ + (e, *validate_exchange(e)) for e in exchanges + ] + return exchanges_valid + + def timeframe_to_seconds(timeframe: str) -> int: """ Translates the timeframe interval value written in the human readable From 969d44a95298e20174514017c85a9925245699ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:49:16 +0200 Subject: [PATCH 0135/1386] Update Dockerfile.armhf --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index b019b6096..dcb008cb9 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.4-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 From ddabfe0206c4fa93efb04942dc4c11c6a062d026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:57:27 +0200 Subject: [PATCH 0136/1386] adjust tests to match new exchangelist output --- tests/commands/test_commands.py | 10 +++++----- tests/test_main.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index e21ef4dd1..232fc4e2c 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -66,8 +66,8 @@ def test_list_exchanges(capsys): start_list_exchanges(get_args(args)) captured = capsys.readouterr() assert re.match(r"Exchanges available for Freqtrade.*", captured.out) - assert re.match(r".*binance,.*", captured.out) - assert re.match(r".*bittrex,.*", captured.out) + assert re.search(r".*binance.*", captured.out) + assert re.search(r".*bittrex.*", captured.out) # Test with --one-column args = [ @@ -89,9 +89,9 @@ def test_list_exchanges(capsys): start_list_exchanges(get_args(args)) captured = capsys.readouterr() assert re.match(r"All exchanges supported by the ccxt library.*", captured.out) - assert re.match(r".*binance,.*", captured.out) - assert re.match(r".*bittrex,.*", captured.out) - assert re.match(r".*bitmex,.*", captured.out) + assert re.search(r".*binance.*", captured.out) + assert re.search(r".*bittrex.*", captured.out) + assert re.search(r".*bitmex.*", captured.out) # Test with --one-column --all args = [ diff --git a/tests/test_main.py b/tests/test_main.py index 70632aeaa..d52dcaf79 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -118,7 +118,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: def test_main_operational_exception1(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch( - 'freqtrade.commands.list_commands.available_exchanges', + 'freqtrade.commands.list_commands.validate_exchanges', MagicMock(side_effect=ValueError('Oh snap!')) ) patched_configuration_load_config_file(mocker, default_conf) @@ -132,7 +132,7 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None: assert log_has('Fatal exception!', caplog) assert not log_has_re(r'SIGINT.*', caplog) mocker.patch( - 'freqtrade.commands.list_commands.available_exchanges', + 'freqtrade.commands.list_commands.validate_exchanges', MagicMock(side_effect=KeyboardInterrupt) ) with pytest.raises(SystemExit): From 142690c93068be58937aaef08dd9004871f237fb Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:05:03 +0200 Subject: [PATCH 0137/1386] resolves freqtrade/freqtrade#4650 --- config_binance.json.example | 99 --------------- docker-compose.yml | 2 +- freqtrade/constants.py | 4 +- .../plugins/pairlist/VolatilityFilter.py | 120 ++++++++++++++++++ 4 files changed, 123 insertions(+), 102 deletions(-) delete mode 100644 config_binance.json.example create mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/config_binance.json.example b/config_binance.json.example deleted file mode 100644 index 4fa615d6d..000000000 --- a/config_binance.json.example +++ /dev/null @@ -1,99 +0,0 @@ -{ - "max_open_trades": 3, - "stake_currency": "BTC", - "stake_amount": 0.05, - "tradable_balance_ratio": 0.99, - "fiat_display_currency": "USD", - "timeframe": "5m", - "dry_run": true, - "cancel_open_orders_on_exit": false, - "unfilledtimeout": { - "buy": 10, - "sell": 30 - }, - "bid_strategy": { - "ask_last_balance": 0.0, - "use_order_book": false, - "order_book_top": 1, - "check_depth_of_market": { - "enabled": false, - "bids_to_ask_delta": 1 - } - }, - "ask_strategy": { - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, - "use_sell_signal": true, - "sell_profit_only": false, - "ignore_roi_if_buy_signal": false - }, - "exchange": { - "name": "binance", - "key": "your_exchange_key", - "secret": "your_exchange_secret", - "ccxt_config": {"enableRateLimit": true}, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 200 - }, - "pair_whitelist": [ - "ALGO/BTC", - "ATOM/BTC", - "BAT/BTC", - "BCH/BTC", - "BRD/BTC", - "EOS/BTC", - "ETH/BTC", - "IOTA/BTC", - "LINK/BTC", - "LTC/BTC", - "NEO/BTC", - "NXS/BTC", - "XMR/BTC", - "XRP/BTC", - "XTZ/BTC" - ], - "pair_blacklist": [ - "BNB/BTC" - ] - }, - "pairlists": [ - {"method": "StaticPairList"} - ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, - "telegram": { - "enabled": false, - "token": "your_telegram_token", - "chat_id": "your_telegram_chat_id" - }, - "api_server": { - "enabled": false, - "listen_ip_address": "127.0.0.1", - "listen_port": 8080, - "verbosity": "error", - "jwt_secret_key": "somethingrandom", - "CORS_origins": [], - "username": "freqtrader", - "password": "SuperSecurePassword" - }, - "bot_name": "freqtrade", - "initial_state": "running", - "forcebuy_enable": false, - "internals": { - "process_throttle_secs": 5 - } -} diff --git a/docker-compose.yml b/docker-compose.yml index 80e194ab2..71572140c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,4 +25,4 @@ services: --logfile /freqtrade/user_data/logs/freqtrade.log --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json - --strategy SampleStrategy + --strategy BinHV45.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..c4a360d18 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter'] + 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 @@ -416,4 +416,4 @@ PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] # Type for trades list -TradeList = List[List] +TradeList = List[List] \ No newline at end of file diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py new file mode 100644 index 000000000..ea1ebeb29 --- /dev/null +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -0,0 +1,120 @@ +""" +Rate of change pairlist filter +""" +import logging +from copy import deepcopy +from typing import Any, Dict, List, Optional + +import sys +import arrow +from cachetools.ttl import TTLCache +from pandas import DataFrame +import numpy as np + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.plugins.pairlist.IPairList import IPairList + + + +logger = logging.getLogger(__name__) + + +class VolatilityFilter(IPairList): + ''' + Filters pairs by volatility + ''' + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._days = pairlistconfig.get('lookback_days', 10) + self._min_volatility = pairlistconfig.get('min_volatility', 0) + self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") + if self._days > exchange.ohlcv_candle_limit('1d'): + raise OperationalException("VolatilityFilter requires lookback_days to not " + "exceed exchange max request size " + f"({exchange.ohlcv_candle_limit('1d')})") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with volatility range " + f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Validate trading range + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days - 1) + .float_timestamp) * 1000 + # Get all candles + candles = {} + if needed_pairs: + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, + cache=False) + + if self._enabled: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + return pairlist + + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, false if it should be removed + """ + # Check symbol in cache + if pair in self._pair_cache: + return self._pair_cache[pair] + + result = False + if daily_candles is not None and not daily_candles.empty: + returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) + returns.fillna(0, inplace=True) + + volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_avg = volatility_series.mean() + + if self._min_volatility <= volatility_avg <= self._max_volatility: + result = True + else: + self.log_once(f"Removed {pair} from whitelist, because volatility " + f"over {self._days} {plural(self._days, 'day')} " + f"is: {volatility_avg:.3f} " + f"which is not in the configured range of " + f"{self._min_volatility}-{self._max_volatility}.", + logger.info) + result = False + self._pair_cache[pair] = result + + return result From 6f02acdbbd8d1f524e552ae9e24a775fbe7071b1 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:39:27 +0200 Subject: [PATCH 0138/1386] Revert "resolves freqtrade/freqtrade#4650" This reverts commit 142690c93068be58937aaef08dd9004871f237fb. --- config_binance.json.example | 99 +++++++++++++++ docker-compose.yml | 2 +- freqtrade/constants.py | 4 +- .../plugins/pairlist/VolatilityFilter.py | 120 ------------------ 4 files changed, 102 insertions(+), 123 deletions(-) create mode 100644 config_binance.json.example delete mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/config_binance.json.example b/config_binance.json.example new file mode 100644 index 000000000..4fa615d6d --- /dev/null +++ b/config_binance.json.example @@ -0,0 +1,99 @@ +{ + "max_open_trades": 3, + "stake_currency": "BTC", + "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "timeframe": "5m", + "dry_run": true, + "cancel_open_orders_on_exit": false, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, + "bid_strategy": { + "ask_last_balance": 0.0, + "use_order_book": false, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy": { + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 1, + "use_sell_signal": true, + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false + }, + "exchange": { + "name": "binance", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 200 + }, + "pair_whitelist": [ + "ALGO/BTC", + "ATOM/BTC", + "BAT/BTC", + "BCH/BTC", + "BRD/BTC", + "EOS/BTC", + "ETH/BTC", + "IOTA/BTC", + "LINK/BTC", + "LTC/BTC", + "NEO/BTC", + "NXS/BTC", + "XMR/BTC", + "XRP/BTC", + "XTZ/BTC" + ], + "pair_blacklist": [ + "BNB/BTC" + ] + }, + "pairlists": [ + {"method": "StaticPairList"} + ], + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "jwt_secret_key": "somethingrandom", + "CORS_origins": [], + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 71572140c..80e194ab2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,4 +25,4 @@ services: --logfile /freqtrade/user_data/logs/freqtrade.log --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json - --strategy BinHV45.py + --strategy SampleStrategy diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c4a360d18..3a2ed98e9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter', 'VolatilityFilter'] + 'SpreadFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 @@ -416,4 +416,4 @@ PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] # Type for trades list -TradeList = List[List] \ No newline at end of file +TradeList = List[List] diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py deleted file mode 100644 index ea1ebeb29..000000000 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Rate of change pairlist filter -""" -import logging -from copy import deepcopy -from typing import Any, Dict, List, Optional - -import sys -import arrow -from cachetools.ttl import TTLCache -from pandas import DataFrame -import numpy as np - -from freqtrade.exceptions import OperationalException -from freqtrade.misc import plural -from freqtrade.plugins.pairlist.IPairList import IPairList - - - -logger = logging.getLogger(__name__) - - -class VolatilityFilter(IPairList): - ''' - Filters pairs by volatility - ''' - - def __init__(self, exchange, pairlistmanager, - config: Dict[str, Any], pairlistconfig: Dict[str, Any], - pairlist_pos: int) -> None: - super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - - self._days = pairlistconfig.get('lookback_days', 10) - self._min_volatility = pairlistconfig.get('min_volatility', 0) - self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) - self._refresh_period = pairlistconfig.get('refresh_period', 1440) - - self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) - - if self._days < 1: - raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") - if self._days > exchange.ohlcv_candle_limit('1d'): - raise OperationalException("VolatilityFilter requires lookback_days to not " - "exceed exchange max request size " - f"({exchange.ohlcv_candle_limit('1d')})") - - @property - def needstickers(self) -> bool: - """ - Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed - as tickers argument to filter_pairlist - """ - return False - - def short_desc(self) -> str: - """ - Short whitelist method description - used for startup-messages - """ - return (f"{self.name} - Filtering pairs with volatility range " - f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") - - def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: - """ - Validate trading range - :param pairlist: pairlist to filter or sort - :param tickers: Tickers (from exchange.get_tickers()). May be cached. - :return: new allowlist - """ - needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] - - since_ms = int(arrow.utcnow() - .floor('day') - .shift(days=-self._days - 1) - .float_timestamp) * 1000 - # Get all candles - candles = {} - if needed_pairs: - candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, - cache=False) - - if self._enabled: - for p in deepcopy(pairlist): - daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None - if not self._validate_pair_loc(p, daily_candles): - pairlist.remove(p) - return pairlist - - def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: - """ - Validate trading range - :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, false if it should be removed - """ - # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] - - result = False - if daily_candles is not None and not daily_candles.empty: - returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) - returns.fillna(0, inplace=True) - - volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) - volatility_avg = volatility_series.mean() - - if self._min_volatility <= volatility_avg <= self._max_volatility: - result = True - else: - self.log_once(f"Removed {pair} from whitelist, because volatility " - f"over {self._days} {plural(self._days, 'day')} " - f"is: {volatility_avg:.3f} " - f"which is not in the configured range of " - f"{self._min_volatility}-{self._max_volatility}.", - logger.info) - result = False - self._pair_cache[pair] = result - - return result From be770a89417b90a82d537ac5f26d2d76ee726a76 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:42:53 +0200 Subject: [PATCH 0139/1386] added VolatilityFilter resolves freqtrade#4650 --- freqtrade/constants.py | 2 +- .../plugins/pairlist/VolatilityFilter.py | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..b98161ff2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter'] + 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py new file mode 100644 index 000000000..97e86bab6 --- /dev/null +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -0,0 +1,120 @@ +""" +Rate of change pairlist filter +""" +import logging +from copy import deepcopy +from typing import Any, Dict, List, Optional + +import sys +import arrow +from cachetools.ttl import TTLCache +from pandas import DataFrame +import numpy as np + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.plugins.pairlist.IPairList import IPairList + + + +logger = logging.getLogger(__name__) + + +class VolatilityFilter(IPairList): + ''' + Filters pairs by volatility + ''' + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._days = pairlistconfig.get('lookback_days', 10) + self._min_volatility = pairlistconfig.get('min_volatility', 0) + self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") + if self._days > exchange.ohlcv_candle_limit('1d'): + raise OperationalException("VolatilityFilter requires lookback_days to not " + "exceed exchange max request size " + f"({exchange.ohlcv_candle_limit('1d')})") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with volatility range " + f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Validate trading range + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days - 1) + .float_timestamp) * 1000 + # Get all candles + candles = {} + if needed_pairs: + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, + cache=False) + + if self._enabled: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + return pairlist + + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, false if it should be removed + """ + # Check symbol in cache + if pair in self._pair_cache: + return self._pair_cache[pair] + + result = False + if daily_candles is not None and not daily_candles.empty: + returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) + returns.fillna(0, inplace=True) + + volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_avg = volatility_series.mean() + + if self._min_volatility <= volatility_avg <= self._max_volatility: + result = True + else: + self.log_once(f"Removed {pair} from whitelist, because volatility " + f"over {self._days} {plural(self._days, 'day')} " + f"is: {volatility_avg:.3f} " + f"which is not in the configured range of " + f"{self._min_volatility}-{self._max_volatility}.", + logger.info) + result = False + self._pair_cache[pair] = result + + return result \ No newline at end of file From 1733e24062339363084200e12f1bbda26f41dde0 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:44:13 +0200 Subject: [PATCH 0140/1386] pyLint adjustment resolves freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 97e86bab6..1913bfcc1 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -117,4 +117,5 @@ class VolatilityFilter(IPairList): result = False self._pair_cache[pair] = result - return result \ No newline at end of file + return result + \ No newline at end of file From 56ef3af42415164400a3f24bd1e9b2754e467491 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 11:59:58 +0200 Subject: [PATCH 0141/1386] Allow comments in pairs files --- freqtrade/configuration/configuration.py | 12 +++++------- freqtrade/configuration/load_config.py | 9 +++++++++ freqtrade/misc.py | 2 +- tests/test_configuration.py | 19 ++++++------------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index a40a4fd83..9acd532cc 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -11,10 +11,10 @@ from freqtrade import constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir -from freqtrade.configuration.load_config import load_config_file +from freqtrade.configuration.load_config import load_config_file, load_file from freqtrade.exceptions import OperationalException from freqtrade.loggers import setup_logging -from freqtrade.misc import deep_merge_dicts, json_load +from freqtrade.misc import deep_merge_dicts from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode @@ -454,9 +454,8 @@ class Configuration: # or if pairs file is specified explicitely if not pairs_file.exists(): raise OperationalException(f'No pairs file found with path "{pairs_file}".') - with pairs_file.open('r') as f: - config['pairs'] = json_load(f) - config['pairs'].sort() + config['pairs'] = load_file(pairs_file) + config['pairs'].sort() return if 'config' in self.args and self.args['config']: @@ -466,7 +465,6 @@ class Configuration: # Fall back to /dl_path/pairs.json pairs_file = config['datadir'] / 'pairs.json' if pairs_file.exists(): - with pairs_file.open('r') as f: - config['pairs'] = json_load(f) + config['pairs'] = load_file(pairs_file) if 'pairs' in config: config['pairs'].sort() diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 726126034..1320a375f 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -38,6 +38,15 @@ def log_config_error_range(path: str, errmsg: str) -> str: return '' +def load_file(path: Path) -> Dict[str, Any]: + try: + with path.open('r') as file: + config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) + except FileNotFoundError: + raise OperationalException(f'File file "{path}" not found!') + return config + + def load_config_file(path: str) -> Dict[str, Any]: """ Loads a config file from the given path diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 7bbc24056..6508363d6 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -81,7 +81,7 @@ def json_load(datafile: IO) -> Any: """ load data with rapidjson Use this to have a consistent experience, - sete number_mode to "NM_NATIVE" for greatest speed + set number_mode to "NM_NATIVE" for greatest speed """ return rapidjson.load(datafile, number_mode=rapidjson.NM_NATIVE) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 15fbab7f8..b8d38dce0 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1038,37 +1038,30 @@ def test_pairlist_resolving_with_config(mocker, default_conf): def test_pairlist_resolving_with_config_pl(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) - load_mock = mocker.patch("freqtrade.configuration.configuration.json_load", - MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) - mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) arglist = [ 'download-data', '--config', 'config.json', - '--pairs-file', 'pairs.json', + '--pairs-file', 'tests/testdata/pairs.json', ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() - - assert load_mock.call_count == 1 - assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert len(config['pairs']) == 23 + assert 'ETH/BTC' in config['pairs'] + assert 'XRP/BTC' in config['pairs'] assert config['exchange']['name'] == default_conf['exchange']['name'] def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) - mocker.patch("freqtrade.configuration.configuration.json_load", - MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) - mocker.patch.object(Path, "exists", MagicMock(return_value=False)) arglist = [ 'download-data', '--config', 'config.json', - '--pairs-file', 'pairs.json', + '--pairs-file', 'tests/testdata/pairs_doesnotexist.json', ] args = Arguments(arglist).get_parsed_arg() @@ -1081,7 +1074,7 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): def test_pairlist_resolving_fallback(mocker): mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) - mocker.patch("freqtrade.configuration.configuration.json_load", + mocker.patch("freqtrade.configuration.configuration.load_file", MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) arglist = [ 'download-data', From bf0886a839eeca7477ade9eeb5b2fa4bc6a28851 Mon Sep 17 00:00:00 2001 From: klara31 Date: Tue, 6 Apr 2021 18:35:30 +0200 Subject: [PATCH 0142/1386] Update constants.py --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..a3cf71553 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -176,7 +176,7 @@ CONF_SCHEMA = { 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, - 'sell_profit_offset': {'type': 'number', 'minimum': 0.0}, + 'sell_profit_offset': {'type': 'number', 'minimum': -100}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} } }, From c40b811f19414042503514ff9fdb97dc062c88cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 19:35:17 +0200 Subject: [PATCH 0143/1386] flush after creating mock trades --- tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index f8c1c5357..4a2106a4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -220,6 +220,9 @@ def create_mock_trades(fee, use_db: bool = True): trade = mock_trade_6(fee) add_trade(trade) + if use_db: + Trade.query.session.flush() + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: From f37fbbf4e115c3373ad6ef0df660ace3c3c7b83f Mon Sep 17 00:00:00 2001 From: klara31 Date: Tue, 6 Apr 2021 19:47:48 +0200 Subject: [PATCH 0144/1386] Update constants.py --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a3cf71553..2d2f9658c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -176,7 +176,7 @@ CONF_SCHEMA = { 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, - 'sell_profit_offset': {'type': 'number', 'minimum': -100}, + 'sell_profit_offset': {'type': 'number'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} } }, From 5ed7828446abcddc7adb93d81886f66ec8877c83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 20:03:38 +0200 Subject: [PATCH 0145/1386] Remove hardcoded list of non-working exchanges --- freqtrade/exchange/common.py | 70 ------------------------------------ 1 file changed, 70 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 90b70d67e..694aa3aa2 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -18,78 +18,8 @@ BAD_EXCHANGES = { "bitmex": "Various reasons.", "bitstamp": "Does not provide history. " "Details in https://github.com/freqtrade/freqtrade/issues/1983", - "hitbtc": "This API cannot be used with Freqtrade. " - "Use `hitbtc2` exchange id to access this exchange.", "phemex": "Does not provide history. ", "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.", - **dict.fromkeys([ - 'adara', - 'anxpro', - 'bigone', - 'coinbase', - 'coinexchange', - 'coinmarketcap', - 'lykke', - 'xbtce', - ], "Does not provide timeframes. ccxt fetchOHLCV: False"), - **dict.fromkeys([ - 'bcex', - 'bit2c', - 'bitbay', - 'bitflyer', - 'bitforex', - 'bithumb', - 'bitso', - 'bitstamp1', - 'bl3p', - 'braziliex', - 'btcbox', - 'btcchina', - 'btctradeim', - 'btctradeua', - 'bxinth', - 'chilebit', - 'coincheck', - 'coinegg', - 'coinfalcon', - 'coinfloor', - 'coingi', - 'coinmate', - 'coinone', - 'coinspot', - 'coolcoin', - 'crypton', - 'deribit', - 'exmo', - 'exx', - 'flowbtc', - 'foxbit', - 'fybse', - # 'hitbtc', - 'ice3x', - 'independentreserve', - 'indodax', - 'itbit', - 'lakebtc', - 'latoken', - 'liquid', - 'livecoin', - 'luno', - 'mixcoins', - 'negociecoins', - 'nova', - 'paymium', - 'southxchange', - 'stronghold', - 'surbitcoin', - 'therock', - 'tidex', - 'vaultoro', - 'vbtc', - 'virwox', - 'yobit', - 'zaif', - ], "Does not provide timeframes. ccxt fetchOHLCV: emulated"), } MAP_EXCHANGE_CHILDCLASS = { From b6599c1da9a75e4ac7dae47bd4d7844ea354d0a6 Mon Sep 17 00:00:00 2001 From: Aleksey Popov Date: Tue, 6 Apr 2021 20:10:52 +0200 Subject: [PATCH 0146/1386] Improve Kraken-specific config description. Added Warning after Kraken rate limit config in order to clearly highlight that it holds delay between requests instead of req\sec rate. --- docs/exchanges.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 4c7e44b06..1c5956088 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -44,6 +44,10 @@ Due to the heavy rate-limiting applied by Kraken, the following configuration se Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine. It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient. +!!! Warning "rateLimit tuning" + Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate. + So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased. + ## Bittrex ### Order types From a3b4667f7c39b6b1cb137a245427441469a3e751 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 20:16:29 +0200 Subject: [PATCH 0147/1386] Update exchange validation to use "validate_exchange". --- freqtrade/configuration/check_exchange.py | 14 +++++++++----- freqtrade/exchange/__init__.py | 4 ++-- freqtrade/exchange/exchange.py | 15 +++++---------- tests/test_configuration.py | 2 +- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index aa36de3ff..832caf153 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -2,8 +2,8 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, is_exchange_bad, - is_exchange_known_ccxt, is_exchange_officially_supported) +from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt, + is_exchange_officially_supported, validate_exchange) from freqtrade.state import RunMode @@ -57,9 +57,13 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: f'{", ".join(available_exchanges())}' ) - if check_for_bad and is_exchange_bad(exchange): - raise OperationalException(f'Exchange "{exchange}" is known to not work with the bot yet. ' - f'Reason: {get_exchange_bad_reason(exchange)}') + valid, reason = validate_exchange(exchange) + if not valid: + if check_for_bad: + raise OperationalException(f'Exchange "{exchange}" will not work with Freqtrade. ' + f'Reason: {reason}') + else: + logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}') if is_exchange_officially_supported(exchange): logger.info(f'Exchange "{exchange}" is officially supported ' diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 0eedd25d8..8a5563623 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -8,10 +8,10 @@ from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, - get_exchange_bad_reason, is_exchange_bad, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds, validate_exchanges) + timeframe_to_seconds, validate_exchange, + validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 768a02aa1..37d92e253 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1307,14 +1307,6 @@ class Exchange: self.calculate_fee_rate(order)) -def is_exchange_bad(exchange_name: str) -> bool: - return exchange_name in BAD_EXCHANGES - - -def get_exchange_bad_reason(exchange_name: str) -> str: - return BAD_EXCHANGES.get(exchange_name, "") - - def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) @@ -1335,18 +1327,21 @@ def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list """ exchanges = ccxt_exchanges(ccxt_module) - return [x for x in exchanges if not is_exchange_bad(x)] + return [x for x in exchanges if validate_exchange(x)[0]] def validate_exchange(exchange: str) -> Tuple[bool, str]: ex_mod = getattr(ccxt, exchange.lower())() if not ex_mod or not ex_mod.has: return False, '' - missing = [k for k in EXCHANGE_HAS_REQUIRED if not ex_mod.has.get(k)] + missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True] if missing: return False, f"missing: {', '.join(missing)}" missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)] + + if exchange.lower() in BAD_EXCHANGES: + return False, BAD_EXCHANGES.get(exchange.lower(), '') if missing_opt: return True, f"missing opt: {', '.join(missing_opt)}" diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 15fbab7f8..e480b7bc3 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -565,7 +565,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) with pytest.raises(OperationalException, - match=r"Exchange .* is known to not work with the bot yet.*"): + match=r"Exchange .* will not work with Freqtrade\..*"): check_exchange(default_conf) caplog.clear() From 187cf6dcd5217914cea52c3296afb9f58728d017 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 22:41:15 +0200 Subject: [PATCH 0148/1386] VolatilityFilter resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 6 +++--- tests/plugins/test_pairlist.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 1913bfcc1..1bb836e76 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -67,7 +67,7 @@ class VolatilityFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new allowlist """ - needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache] since_ms = int(arrow.utcnow() .floor('day') @@ -81,7 +81,7 @@ class VolatilityFilter(IPairList): if self._enabled: for p in deepcopy(pairlist): - daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None if not self._validate_pair_loc(p, daily_candles): pairlist.remove(p) return pairlist @@ -102,7 +102,7 @@ class VolatilityFilter(IPairList): returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) returns.fillna(0, inplace=True) - volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_series = returns.rolling(window=self._days).std()*np.sqrt(self._days) volatility_avg = volatility_series.mean() if self._min_volatility <= volatility_avg <= self._max_volatility: diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 67cd96f5b..7d39014f1 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -407,6 +407,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01, "refresh_period": 1440}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), + ([{"method": "StaticPairList"}, + {"method": "VolatilityFilter", "lookback_days": 3, + "min_volatility": 0.002, "max_volatility": 0.004, "refresh_period": 1440}], + "BTC", ['ETH/BTC', 'TKN/BTC']) ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history, pairlists, base_currency, @@ -414,13 +418,19 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t whitelist_conf['pairlists'] = pairlists whitelist_conf['stake_currency'] = base_currency + ohlcv_history_high_vola = ohlcv_history.copy() + ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index==1, 'close'] = 0.00090 + ohlcv_data = { ('ETH/BTC', '1d'): ohlcv_history, ('TKN/BTC', '1d'): ohlcv_history, ('LTC/BTC', '1d'): ohlcv_history, ('XRP/BTC', '1d'): ohlcv_history, - ('HOT/BTC', '1d'): ohlcv_history, + ('HOT/BTC', '1d'): ohlcv_history_high_vola, } + + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) @@ -487,7 +497,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert log_has(logmsg, caplog) else: assert not log_has(logmsg, caplog) - + if pairlist["method"] == 'VolatilityFilter': + assert log_has_re(r'^Removed .* from whitelist, because volatility.*$', caplog) def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] From 9089323d266980c1d896c35341e3fd3e68dd8362 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 22:46:36 +0200 Subject: [PATCH 0149/1386] resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 1bb836e76..5b50e04e4 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -1,5 +1,5 @@ """ -Rate of change pairlist filter +Volatility pairlist filter """ import logging from copy import deepcopy From 9772a93634a1c60be321cbb78a51cec52cd59a5c Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 23:11:40 +0200 Subject: [PATCH 0150/1386] resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 5 ++--- tests/plugins/test_pairlist.py | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 5b50e04e4..6ef3841f5 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -16,7 +16,6 @@ from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList - logger = logging.getLogger(__name__) @@ -58,7 +57,8 @@ class VolatilityFilter(IPairList): Short whitelist method description - used for startup-messages """ return (f"{self.name} - Filtering pairs with volatility range " - f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + f"{self._min_volatility}-{self._max_volatility} " + f" the last {self._days} {plural(self._days, 'day')}.") def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ @@ -118,4 +118,3 @@ class VolatilityFilter(IPairList): self._pair_cache[pair] = result return result - \ No newline at end of file diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 7d39014f1..4db0b7098 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -419,7 +419,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t whitelist_conf['stake_currency'] = base_currency ohlcv_history_high_vola = ohlcv_history.copy() - ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index==1, 'close'] = 0.00090 + ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, 'close'] = 0.00090 ohlcv_data = { ('ETH/BTC', '1d'): ohlcv_history, @@ -428,15 +428,12 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ('XRP/BTC', '1d'): ohlcv_history, ('HOT/BTC', '1d'): ohlcv_history_high_vola, } - - - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) if whitelist_result == 'static_in_the_middle': with pytest.raises(OperationalException, - match=r"StaticPairList can only be used in the first position " + match=r"StaticPairList only in the first position " r"in the list of Pairlist Handlers."): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) return @@ -500,6 +497,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t if pairlist["method"] == 'VolatilityFilter': assert log_has_re(r'^Removed .* from whitelist, because volatility.*$', caplog) + def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] del whitelist_conf['stoploss'] From 0f0607baecdbbc04378d546239265d2e22c52c02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:52:34 +0200 Subject: [PATCH 0151/1386] Fix rangeestability filter caching issue --- freqtrade/plugins/pairlist/rangestabilityfilter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index a1430a223..6565e92c1 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -87,8 +87,9 @@ class RangeStabilityFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] + cached_res = self._pair_cache.get(pair, None) + if cached_res is not None: + return cached_res result = False if daily_candles is not None and not daily_candles.empty: From ac6bff536f8c5d5bc78b4cd4cdd499bba7af2e89 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:55:11 +0200 Subject: [PATCH 0152/1386] Fix test failure with UI test if UI is deployed --- freqtrade/rpc/api_server/web_ui.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 13d22a63e..a8c737e04 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -13,6 +13,11 @@ async def favicon(): return FileResponse(str(Path(__file__).parent / 'ui/favicon.ico')) +@router_ui.get('/fallback_file.html', include_in_schema=False) +async def fallback(): + return FileResponse(str(Path(__file__).parent / 'ui/fallback_file.html')) + + @router_ui.get('/{rest_of_path:path}', include_in_schema=False) async def index_html(rest_of_path: str): """ From d2680f6cb8c63de6470864b675fccdcb888ff01d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:57:05 +0200 Subject: [PATCH 0153/1386] Remove telegram deprecation warning closes #4688 --- freqtrade/rpc/telegram.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b418c3dab..17ddd1c91 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -159,10 +159,10 @@ class Telegram(RPCHandler): for handle in handles: self._updater.dispatcher.add_handler(handle) self._updater.start_polling( - clean=True, bootstrap_retries=-1, timeout=30, read_latency=60, + drop_pending_updates=True, ) logger.info( 'rpc.telegram is listening for following commands: %s', diff --git a/setup.py b/setup.py index bf23fb999..54a2e01b5 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup(name='freqtrade', # from requirements.txt 'ccxt>=1.24.96', 'SQLAlchemy', - 'python-telegram-bot', + 'python-telegram-bot>=13.4', 'arrow>=0.17.0', 'cachetools', 'requests', From 7f8d90d34c37549937dd4ceb2fde565b63c03213 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 07:05:10 +0200 Subject: [PATCH 0154/1386] Update list-exchanges doc with new format --- docs/utils.md | 197 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 2 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index a84f068e9..8ef12e1c9 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -253,13 +253,206 @@ optional arguments: * Example: see exchanges available for the bot: ``` $ freqtrade list-exchanges -Exchanges available for Freqtrade: _1btcxe, acx, allcoin, bequant, bibox, binance, binanceje, binanceus, bitbank, bitfinex, bitfinex2, bitkk, bitlish, bitmart, bittrex, bitz, bleutrade, btcalpha, btcmarkets, btcturk, buda, cex, cobinhood, coinbaseprime, coinbasepro, coinex, cointiger, coss, crex24, digifinex, dsx, dx, ethfinex, fcoin, fcoinjp, gateio, gdax, gemini, hitbtc2, huobipro, huobiru, idex, kkex, kraken, kucoin, kucoin2, kuna, lbank, mandala, mercado, oceanex, okcoincny, okcoinusd, okex, okex3, poloniex, rightbtc, theocean, tidebit, upbit, zb +Exchanges available for Freqtrade: +Exchange name Valid reason +--------------- ------- -------------------------------------------- +aax True +ascendex True missing opt: fetchMyTrades +bequant True +bibox True +bigone True +binance True +binanceus True +bitbank True missing opt: fetchTickers +bitcoincom True +bitfinex True +bitforex True missing opt: fetchMyTrades, fetchTickers +bitget True +bithumb True missing opt: fetchMyTrades +bitkk True missing opt: fetchMyTrades +bitmart True +bitmax True missing opt: fetchMyTrades +bitpanda True +bittrex True +bitvavo True +bitz True missing opt: fetchMyTrades +btcalpha True missing opt: fetchTicker, fetchTickers +btcmarkets True missing opt: fetchTickers +buda True missing opt: fetchMyTrades, fetchTickers +bw True missing opt: fetchMyTrades, fetchL2OrderBook +bybit True +bytetrade True +cdax True +cex True missing opt: fetchMyTrades +coinbaseprime True missing opt: fetchTickers +coinbasepro True missing opt: fetchTickers +coinex True +crex24 True +deribit True +digifinex True +equos True missing opt: fetchTicker, fetchTickers +eterbase True +fcoin True missing opt: fetchMyTrades, fetchTickers +fcoinjp True missing opt: fetchMyTrades, fetchTickers +ftx True +gateio True +gemini True +gopax True +hbtc True +hitbtc True +huobijp True +huobipro True +idex True +kraken True +kucoin True +lbank True missing opt: fetchMyTrades +mercado True missing opt: fetchTickers +ndax True missing opt: fetchTickers +novadax True +okcoin True +okex True +probit True +qtrade True +stex True +timex True +upbit True missing opt: fetchMyTrades +vcc True +zb True missing opt: fetchMyTrades + ``` +!!! Note "missing opt exchanges" + Values with "missing opt:" might need special configuration (e.g. using orderbook if `fetchTickers` is missing) - but should in theory work (although we cannot guarantee they will). + * Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade): ``` $ freqtrade list-exchanges -a -All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpro, bcex, bequant, bibox, bigone, binance, binanceje, binanceus, bit2c, bitbank, bitbay, bitfinex, bitfinex2, bitflyer, bitforex, bithumb, bitkk, bitlish, bitmart, bitmex, bitso, bitstamp, bitstamp1, bittrex, bitz, bl3p, bleutrade, braziliex, btcalpha, btcbox, btcchina, btcmarkets, btctradeim, btctradeua, btcturk, buda, bxinth, cex, chilebit, cobinhood, coinbase, coinbaseprime, coinbasepro, coincheck, coinegg, coinex, coinexchange, coinfalcon, coinfloor, coingi, coinmarketcap, coinmate, coinone, coinspot, cointiger, coolcoin, coss, crex24, crypton, deribit, digifinex, dsx, dx, ethfinex, exmo, exx, fcoin, fcoinjp, flowbtc, foxbit, fybse, gateio, gdax, gemini, hitbtc, hitbtc2, huobipro, huobiru, ice3x, idex, independentreserve, indodax, itbit, kkex, kraken, kucoin, kucoin2, kuna, lakebtc, latoken, lbank, liquid, livecoin, luno, lykke, mandala, mercado, mixcoins, negociecoins, nova, oceanex, okcoincny, okcoinusd, okex, okex3, paymium, poloniex, rightbtc, southxchange, stronghold, surbitcoin, theocean, therock, tidebit, tidex, upbit, vaultoro, vbtc, virwox, xbtce, yobit, zaif, zb +All exchanges supported by the ccxt library: +Exchange name Valid reason +------------------ ------- --------------------------------------------------------------------------------------- +aax True +aofex False missing: fetchOrder +ascendex True missing opt: fetchMyTrades +bequant True +bibox True +bigone True +binance True +binanceus True +bit2c False missing: fetchOrder, fetchOHLCV +bitbank True missing opt: fetchTickers +bitbay False missing: fetchOrder +bitcoincom True +bitfinex True +bitfinex2 False missing: fetchOrder +bitflyer False missing: fetchOrder, fetchOHLCV +bitforex True missing opt: fetchMyTrades, fetchTickers +bitget True +bithumb True missing opt: fetchMyTrades +bitkk True missing opt: fetchMyTrades +bitmart True +bitmax True missing opt: fetchMyTrades +bitmex False Various reasons. +bitpanda True +bitso False missing: fetchOHLCV +bitstamp False Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983 +bitstamp1 False missing: fetchOrder, fetchOHLCV +bittrex True +bitvavo True +bitz True missing opt: fetchMyTrades +bl3p False missing: fetchOrder, fetchOHLCV +bleutrade False missing: fetchOrder +braziliex False missing: fetchOHLCV +btcalpha True missing opt: fetchTicker, fetchTickers +btcbox False missing: fetchOHLCV +btcmarkets True missing opt: fetchTickers +btctradeua False missing: fetchOrder, fetchOHLCV +btcturk False missing: fetchOrder +buda True missing opt: fetchMyTrades, fetchTickers +bw True missing opt: fetchMyTrades, fetchL2OrderBook +bybit True +bytetrade True +cdax True +cex True missing opt: fetchMyTrades +chilebit False missing: fetchOrder, fetchOHLCV +coinbase False missing: fetchOrder, cancelOrder, createOrder, fetchOHLCV +coinbaseprime True missing opt: fetchTickers +coinbasepro True missing opt: fetchTickers +coincheck False missing: fetchOrder, fetchOHLCV +coinegg False missing: fetchOHLCV +coinex True +coinfalcon False missing: fetchOHLCV +coinfloor False missing: fetchOrder, fetchOHLCV +coingi False missing: fetchOrder, fetchOHLCV +coinmarketcap False missing: fetchOrder, cancelOrder, createOrder, fetchBalance, fetchOHLCV +coinmate False missing: fetchOHLCV +coinone False missing: fetchOHLCV +coinspot False missing: fetchOrder, cancelOrder, fetchOHLCV +crex24 True +currencycom False missing: fetchOrder +delta False missing: fetchOrder +deribit True +digifinex True +equos True missing opt: fetchTicker, fetchTickers +eterbase True +exmo False missing: fetchOrder +exx False missing: fetchOHLCV +fcoin True missing opt: fetchMyTrades, fetchTickers +fcoinjp True missing opt: fetchMyTrades, fetchTickers +flowbtc False missing: fetchOrder, fetchOHLCV +foxbit False missing: fetchOrder, fetchOHLCV +ftx True +gateio True +gemini True +gopax True +hbtc True +hitbtc True +hollaex False missing: fetchOrder +huobijp True +huobipro True +idex True +independentreserve False missing: fetchOHLCV +indodax False missing: fetchOHLCV +itbit False missing: fetchOHLCV +kraken True +kucoin True +kuna False missing: fetchOHLCV +lakebtc False missing: fetchOrder, fetchOHLCV +latoken False missing: fetchOrder, fetchOHLCV +lbank True missing opt: fetchMyTrades +liquid False missing: fetchOHLCV +luno False missing: fetchOHLCV +lykke False missing: fetchOHLCV +mercado True missing opt: fetchTickers +mixcoins False missing: fetchOrder, fetchOHLCV +ndax True missing opt: fetchTickers +novadax True +oceanex False missing: fetchOHLCV +okcoin True +okex True +paymium False missing: fetchOrder, fetchOHLCV +phemex False Does not provide history. +poloniex False missing: fetchOrder +probit True +qtrade True +rightbtc False missing: fetchOrder +ripio False missing: fetchOHLCV +southxchange False missing: fetchOrder, fetchOHLCV +stex True +surbitcoin False missing: fetchOrder, fetchOHLCV +therock False missing: fetchOHLCV +tidebit False missing: fetchOrder +tidex False missing: fetchOHLCV +timex True +upbit True missing opt: fetchMyTrades +vbtc False missing: fetchOrder, fetchOHLCV +vcc True +wavesexchange False missing: fetchOrder +whitebit False missing: fetchOrder, cancelOrder, createOrder, fetchBalance +xbtce False missing: fetchOrder, fetchOHLCV +xena False missing: fetchOrder +yobit False missing: fetchOHLCV +zaif False missing: fetchOrder, fetchOHLCV +zb True missing opt: fetchMyTrades ``` ## List Timeframes From 17508efbbc88e89a7380cb6d14f12c5350e0ed5c Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 08:59:44 +0200 Subject: [PATCH 0155/1386] resolves freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 33 ++++++++++++++++++++++++++++++++- tests/plugins/test_pairlist.py | 2 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 2653406e7..ad1ac6efc 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -4,7 +4,7 @@ Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). -Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. +Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. @@ -29,6 +29,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged * [`ShuffleFilter`](#shufflefilter) * [`SpreadFilter`](#spreadfilter) * [`RangeStabilityFilter`](#rangestabilityfilter) +* [`VolatilityFilter`](#volatilityfilter) !!! Tip "Testing pairlists" Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. @@ -164,6 +165,29 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit !!! Tip This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. +#### VolatilityFilter + +Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. + +Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. + +This filter can be used to narrow down your pairs to a certain volatilty or avoid very volatile pairs. + +In the below example: +If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. + +```json +"pairlists": [ + { + "method": "VolatilityFilter", + "lookback_days": 10, + "min_volatilty": 0.20, + "max_volatilty": 0.30, + "refresh_period": 86400 + } +] +``` + ### Full example of Pairlist Handlers The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. @@ -189,6 +213,13 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, "min_rate_of_change": 0.01, "refresh_period": 1440 }, + { + "method": "VolatilityFilter", + "lookback_days": 10, + "min_volatilty": 0.20, + "max_volatilty": 0.30, + "refresh_period": 86400 + }, {"method": "ShuffleFilter", "seed": 42} ], ``` diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 4db0b7098..bf225271f 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -433,7 +433,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t if whitelist_result == 'static_in_the_middle': with pytest.raises(OperationalException, - match=r"StaticPairList only in the first position " + match=r"StaticPairList can only be used in the first position " r"in the list of Pairlist Handlers."): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) return From 5ee879a747a5bbb916601c90af1e0cb25de56515 Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 10:15:51 +0200 Subject: [PATCH 0156/1386] isort resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 6ef3841f5..f8e380f56 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -2,14 +2,14 @@ Volatility pairlist filter """ import logging +import sys from copy import deepcopy from typing import Any, Dict, List, Optional -import sys import arrow +import numpy as np from cachetools.ttl import TTLCache from pandas import DataFrame -import numpy as np from freqtrade.exceptions import OperationalException from freqtrade.misc import plural From 4d30c32ad2bda337da4efb1f2d87bb76dc9693e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 17:10:20 +0200 Subject: [PATCH 0157/1386] Improve resiliancy of a test --- tests/conftest_trades.py | 4 +- tests/rpc/test_rpc_apiserver.py | 72 ++++++++++++++++----------------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 8e4be9165..34fc58aee 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -241,7 +241,8 @@ def mock_trade_5(fee): open_rate=0.123, exchange='bittrex', strategy='SampleStrategy', - stoploss_order_id='prod_stoploss_3455' + stoploss_order_id='prod_stoploss_3455', + timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy') trade.orders.append(o) @@ -295,6 +296,7 @@ def mock_trade_6(fee): exchange='bittrex', strategy='SampleStrategy', open_order_id="prod_sell_6", + timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a65b4ed6f..d113a8802 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -753,25 +753,21 @@ def test_api_status(botclient, mocker, ticker, fee, markets): get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) + markets=PropertyMock(return_value=markets), + fetch_order=MagicMock(return_value={}), ) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 200) assert rc.json() == [] - - ftbot.enter_positions() - trades = Trade.get_open_trades() - trades[0].open_order_id = None - ftbot.exit_positions(trades) - Trade.query.session.flush() + create_mock_trades(fee) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) - assert len(rc.json()) == 1 - assert rc.json() == [{ - 'amount': 91.07468123, - 'amount_requested': 91.07468123, + assert len(rc.json()) == 4 + assert rc.json()[0] == { + 'amount': 123.0, + 'amount_requested': 123.0, 'base_currency': 'BTC', 'close_date': None, 'close_date_hum': None, @@ -780,37 +776,37 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'close_profit_pct': None, 'close_profit_abs': None, 'close_rate': None, - 'current_profit': -0.00408133, - 'current_profit_pct': -0.41, - 'current_profit_abs': -4.09e-06, - 'profit_ratio': -0.00408133, - 'profit_pct': -0.41, - 'profit_abs': -4.09e-06, + 'current_profit': ANY, + 'current_profit_pct': ANY, + 'current_profit_abs': ANY, + 'profit_ratio': ANY, + 'profit_pct': ANY, + 'profit_abs': ANY, 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, - 'open_date_hum': 'just now', + 'open_date_hum': ANY, 'open_timestamp': ANY, 'open_order': None, - 'open_rate': 1.098e-05, + 'open_rate': 0.123, 'pair': 'ETH/BTC', 'stake_amount': 0.001, - 'stop_loss_abs': 9.882e-06, - 'stop_loss_pct': -10.0, - 'stop_loss_ratio': -0.1, + 'stop_loss_abs': ANY, + 'stop_loss_pct': ANY, + 'stop_loss_ratio': ANY, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, - 'initial_stop_loss_abs': 9.882e-06, - 'initial_stop_loss_pct': -10.0, - 'initial_stop_loss_ratio': -0.1, - 'stoploss_current_dist': -1.1080000000000002e-06, - 'stoploss_current_dist_ratio': -0.10081893, - 'stoploss_current_dist_pct': -10.08, - 'stoploss_entry_dist': -0.00010475, - 'stoploss_entry_dist_ratio': -0.10448878, + 'initial_stop_loss_abs': 0.0, + 'initial_stop_loss_pct': ANY, + 'initial_stop_loss_ratio': ANY, + 'stoploss_current_dist': ANY, + 'stoploss_current_dist_ratio': ANY, + 'stoploss_current_dist_pct': ANY, + 'stoploss_entry_dist': ANY, + 'stoploss_entry_dist_ratio': ANY, 'trade_id': 1, - 'close_rate_requested': None, + 'close_rate_requested': ANY, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, @@ -818,17 +814,17 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'fee_open_cost': None, 'fee_open_currency': None, 'is_open': True, - 'max_rate': 1.099e-05, - 'min_rate': 1.098e-05, - 'open_order_id': None, - 'open_rate_requested': 1.098e-05, - 'open_trade_value': 0.0010025, + 'max_rate': ANY, + 'min_rate': ANY, + 'open_order_id': 'dry_run_buy_12345', + 'open_rate_requested': ANY, + 'open_trade_value': 15.1668225, 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, 'exchange': 'bittrex', - }] + } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) @@ -836,7 +832,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) resp_values = rc.json() - assert len(resp_values) == 1 + assert len(resp_values) == 4 assert isnan(resp_values[0]['profit_abs']) From f8244d9d76610c96e0995754a934272bf94a536f Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 22:25:54 +0200 Subject: [PATCH 0158/1386] resolves freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 6 +++--- freqtrade/plugins/pairlist/VolatilityFilter.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index ad1ac6efc..8c65753b6 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,7 +167,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. +Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. @@ -181,8 +181,8 @@ If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove { "method": "VolatilityFilter", "lookback_days": 10, - "min_volatilty": 0.20, - "max_volatilty": 0.30, + "min_volatility": 0.05, + "max_volatility": 0.50, "refresh_period": 86400 } ] diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index f8e380f56..400b1577d 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -94,8 +94,9 @@ class VolatilityFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] + cached_res = self._pair_cache.get(pair, None) + if cached_res is not None: + return cached_res result = False if daily_candles is not None and not daily_candles.empty: From 862f69f895c4dd1774ba78ca9a221692ad9bb930 Mon Sep 17 00:00:00 2001 From: gbojen Date: Thu, 8 Apr 2021 16:43:38 +0200 Subject: [PATCH 0159/1386] removed typos --- docs/includes/pairlists.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8c65753b6..3aa2c63f4 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,14 +167,14 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). +Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). -Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. +This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. -This filter can be used to narrow down your pairs to a certain volatilty or avoid very volatile pairs. +This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. In the below example: -If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. +If the volatility over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. ```json "pairlists": [ From 5a5c5fccf236bf23612af897527fb7f86f588222 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 16:45:52 +0200 Subject: [PATCH 0160/1386] Add gitattributes file --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..00abd1d9d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.py eol=lf +*.sh eol=lf +*.ps1 eol=crlf From 74bf0b6399adf9766f8af6b2c68140723d0c3d51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 19:29:51 +0200 Subject: [PATCH 0161/1386] Fix typo in documentation --- docs/includes/pairlists.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 3aa2c63f4..d57757bbd 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,7 +167,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). +Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. @@ -216,8 +216,8 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, { "method": "VolatilityFilter", "lookback_days": 10, - "min_volatilty": 0.20, - "max_volatilty": 0.30, + "min_volatility": 0.05, + "max_volatility": 0.50, "refresh_period": 86400 }, {"method": "ShuffleFilter", "seed": 42} From 898c24949bfcaee24e08198566afb15125679c4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:07:52 +0200 Subject: [PATCH 0162/1386] Add chown method to support docker --- freqtrade/commands/build_config_commands.py | 2 ++ .../configuration/directory_operations.py | 16 +++++++++++++ tests/test_directory_operations.py | 23 +++++++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 0dee480b3..03d095e12 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List from questionary import Separator, prompt +from freqtrade.configuration.directory_operations import chown_user_directory from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import OperationalException from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges @@ -216,6 +217,7 @@ def start_new_config(args: Dict[str, Any]) -> None: """ config_path = Path(args['config'][0]) + chown_user_directory(config_path.parent) if config_path.exists(): overwrite = ask_user_overwrite(config_path) if overwrite: diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 1ce8d1461..ca305c260 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -24,6 +24,21 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat return folder +def chown_user_directory(directory: Path) -> None: + """ + Use Sudo to change permissions of the home-directory if necessary + Only applies when running in docker! + """ + import os + if os.environ.get('FT_APP_ENV') == 'docker': + try: + import subprocess + subprocess.check_output( + ['sudo', 'chown', '-R', 'ftuser:', str(directory.resolve())]) + except Exception: + logger.warning(f"Could not chown {directory}") + + def create_userdata_dir(directory: str, create_dir: bool = False) -> Path: """ Create userdata directory structure. @@ -37,6 +52,7 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path: sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "logs", "notebooks", "plot", "strategies", ] folder = Path(directory) + chown_user_directory(folder) if not folder.is_dir(): if create_dir: folder.mkdir(parents=True) diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index a8058c514..a11200526 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -1,11 +1,12 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name +import os from pathlib import Path from unittest.mock import MagicMock import pytest -from freqtrade.configuration.directory_operations import (copy_sample_files, create_datadir, - create_userdata_dir) +from freqtrade.configuration.directory_operations import (chown_user_directory, copy_sample_files, + create_datadir, create_userdata_dir) from freqtrade.exceptions import OperationalException from tests.conftest import log_has, log_has_re @@ -31,6 +32,24 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None: assert str(x) == str(Path("/tmp/bar")) +def test_create_userdata_dir_and_chown(mocker, tmpdir, caplog) -> None: + sp_mock = mocker.patch('subprocess.check_output') + path = Path(tmpdir / 'bar') + assert not path.is_dir() + + x = create_userdata_dir(str(path), create_dir=True) + assert sp_mock.call_count == 0 + assert log_has(f'Created user-data directory: {path}', caplog) + assert isinstance(x, Path) + assert path.is_dir() + assert (path / 'data').is_dir() + + os.environ['FT_APP_ENV'] = 'docker' + chown_user_directory(path / 'data') + assert sp_mock.call_count == 1 + del os.environ['FT_APP_ENV'] + + def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) md = mocker.patch.object(Path, 'mkdir', MagicMock()) From 4eb251ce416cf3a3bf40071b8843814f38726cfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:17:53 +0200 Subject: [PATCH 0163/1386] Update dockerfiles to run as non-root --- Dockerfile | 26 ++++++++++++++++++-------- Dockerfile.armhf | 28 +++++++++++++++++----------- docker/Dockerfile.custom | 8 ++++---- docker/Dockerfile.develop | 4 ++-- docker/Dockerfile.jupyter | 2 +- docker/Dockerfile.plot | 2 +- 6 files changed, 43 insertions(+), 27 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b399174b..ac48ea611 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,19 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 -ENV PATH=/root/.local/bin:$PATH +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" # Prepare environment -RUN mkdir /freqtrade +RUN mkdir /freqtrade \ + && apt update \ + && apt install -y sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + WORKDIR /freqtrade # Install dependencies @@ -24,7 +33,8 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt requirements-hyperopt.txt /freqtrade/ +COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ +USER ftuser RUN pip install --user --no-cache-dir numpy \ && pip install --user --no-cache-dir -r requirements-hyperopt.txt @@ -33,13 +43,13 @@ FROM base as runtime-image COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib -COPY --from=python-deps /root/.local /root/.local - - +COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +USER ftuser # Install and execute -COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir \ +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ && mkdir /freqtrade/user_data/ \ && freqtrade install-ui diff --git a/Dockerfile.armhf b/Dockerfile.armhf index eecd9fdc0..62ef165c0 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -5,15 +5,20 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 -ENV PATH=/root/.local/bin:$PATH +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" # Prepare environment -RUN mkdir /freqtrade -WORKDIR /freqtrade +RUN mkdir /freqtrade \ + && apt-get update \ + && apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -RUN apt-get update \ - && apt-get -y install libatlas3-base curl sqlite3 \ - && apt-get clean +WORKDIR /freqtrade # Install dependencies FROM base as python-deps @@ -37,13 +42,14 @@ FROM base as runtime-image COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib -COPY --from=python-deps /root/.local /root/.local +COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +USER ftuser # Install and execute -COPY . /freqtrade/ -RUN apt-get install -y libhdf5-serial-dev \ - && apt-get clean \ - && pip install -e . --no-cache-dir \ +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ + && mkdir /freqtrade/user_data/ \ && freqtrade install-ui ENTRYPOINT ["freqtrade"] diff --git a/docker/Dockerfile.custom b/docker/Dockerfile.custom index 10620e6b8..a7c599fa8 100644 --- a/docker/Dockerfile.custom +++ b/docker/Dockerfile.custom @@ -1,7 +1,7 @@ FROM freqtradeorg/freqtrade:develop -RUN apt-get update \ - && apt-get -y install git \ - && apt-get clean \ +RUN sudo apt-get update \ + && sudo apt-get -y install git \ + && sudo apt-get clean \ # The below dependency - pyti - serves as an example. Please use whatever you need! - && pip install pyti + && pip install --user pyti diff --git a/docker/Dockerfile.develop b/docker/Dockerfile.develop index cb49984e2..7c580f234 100644 --- a/docker/Dockerfile.develop +++ b/docker/Dockerfile.develop @@ -3,8 +3,8 @@ FROM freqtradeorg/freqtrade:develop # Install dependencies COPY requirements-dev.txt /freqtrade/ -RUN pip install numpy --no-cache-dir \ - && pip install -r requirements-dev.txt --no-cache-dir +RUN pip install numpy --user --no-cache-dir \ + && pip install -r requirements-dev.txt --user --no-cache-dir # Empty the ENTRYPOINT to allow all commands ENTRYPOINT [] diff --git a/docker/Dockerfile.jupyter b/docker/Dockerfile.jupyter index b7499eeef..7d603c667 100644 --- a/docker/Dockerfile.jupyter +++ b/docker/Dockerfile.jupyter @@ -1,7 +1,7 @@ FROM freqtradeorg/freqtrade:develop_plot -RUN pip install jupyterlab --no-cache-dir +RUN pip install jupyterlab --user --no-cache-dir # Empty the ENTRYPOINT to allow all commands ENTRYPOINT [] diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index 40bc72bc5..d2fc3618a 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -4,4 +4,4 @@ FROM freqtradeorg/freqtrade:${sourceimage} # Install dependencies COPY requirements-plot.txt /freqtrade/ -RUN pip install -r requirements-plot.txt --no-cache-dir +RUN pip install -r requirements-plot.txt --user --no-cache-dir From 644dcc1641624700033b0865f8360a5264e51585 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:36:10 +0200 Subject: [PATCH 0164/1386] Only allow chown via sudo --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- docker/Dockerfile.custom | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index ac48ea611..a6dc9a991 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN mkdir /freqtrade \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ # Allow sudoers - && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers WORKDIR /freqtrade diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 62ef165c0..909c44eaa 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -16,7 +16,7 @@ RUN mkdir /freqtrade \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ # Allow sudoers - && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers WORKDIR /freqtrade diff --git a/docker/Dockerfile.custom b/docker/Dockerfile.custom index a7c599fa8..3b55fcb0e 100644 --- a/docker/Dockerfile.custom +++ b/docker/Dockerfile.custom @@ -1,7 +1,10 @@ FROM freqtradeorg/freqtrade:develop -RUN sudo apt-get update \ - && sudo apt-get -y install git \ - && sudo apt-get clean \ - # The below dependency - pyti - serves as an example. Please use whatever you need! - && pip install --user pyti +# Switch user to root if you must install something from apt +# Don't forget to switch the user back below! +# USER root + +# The below dependency - pyti - serves as an example. Please use whatever you need! +RUN pip install --user pyti + +# USER ftuser From 0b4b67e46b444167d18d37657521d7c212781c92 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Fri, 9 Apr 2021 10:36:03 +0900 Subject: [PATCH 0165/1386] add FAQ entries for shorting, futures, and options --- docs/faq.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 93b806dca..c890caf1a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,5 +1,19 @@ # Freqtrade FAQ +## Supported Markets + +Freqtrade supports spot trading only. + +### Can I open short positions? + +No, Freqtrade does not support trading with margin / leverage, and cannot open short positions. + +In some cases, your exchange may provide leveraged spot tokens which can be traded with Freqtrade eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD, etc... + +### Can I trade options or futures? + +No, options and futures trading are not supported. + ## Beginner Tips & Tricks * When you work with your strategy & hyperopt file you should use a proper code editor like VSCode or PyCharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely pointed out by Freqtrade during startup). From 4b2cec22ec645bd8a2ef0d75db1a3f2f0b586b1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 19:33:40 +0200 Subject: [PATCH 0166/1386] Chown .local dir --- .dockerignore | 12 ++++++++++-- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index 09f4c9f0c..889a4dfc7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,8 @@ .git .gitignore Dockerfile +Dockerfile.armhf .dockerignore -config.json* -*.sqlite .coveragerc .eggs .github @@ -13,4 +12,13 @@ CONTRIBUTING.md MANIFEST.in README.md freqtrade.service +freqtrade.egg-info + +config.json* +*.sqlite user_data +*.log + +.vscode +.mypy_cache +.ipynb_checkpoints diff --git a/Dockerfile b/Dockerfile index a6dc9a991..5d8bcdd41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ FROM base as runtime-image COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib -COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local USER ftuser # Install and execute diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 909c44eaa..df1c4bc80 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -42,7 +42,7 @@ FROM base as runtime-image COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib -COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local USER ftuser # Install and execute From 126127c1e11a09a4d4653c0ca9551ffc8cf35ab3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 21:28:38 +0200 Subject: [PATCH 0167/1386] Fix armHF image to use ftuser on install too --- Dockerfile.armhf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index df1c4bc80..332b91b35 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -33,7 +33,8 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt /freqtrade/ +COPY --chown=ftuser:ftuser requirements.txt /freqtrade/ +USER ftuser RUN pip install --user --no-cache-dir numpy \ && pip install --user --no-cache-dir -r requirements.txt From 5f67400649d40637177a4a5071e364e94cdea0e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 21:58:15 +0200 Subject: [PATCH 0168/1386] Add SKDecimal Space --- freqtrade/optimize/decimalspace.py | 33 ++++++++++++++++++++++++++++++ freqtrade/strategy/hyper.py | 16 ++++----------- 2 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 freqtrade/optimize/decimalspace.py diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/decimalspace.py new file mode 100644 index 000000000..fbc6a7af6 --- /dev/null +++ b/freqtrade/optimize/decimalspace.py @@ -0,0 +1,33 @@ +import numpy as np + +from skopt.space import Integer + + +class SKDecimal(Integer): + + def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None, + name=None, dtype=np.int64): + self.decimals = decimals + _low = int(low * pow(10, self.decimals)) + _high = int(high * pow(10, self.decimals)) + self.low_orig = low + self.high_orig = high + + super().__init__(_low, _high, prior, base, transform, name, dtype) + + def __repr__(self): + return "Decimal(low={}, high={}, decimals={}, prior='{}', transform='{}')".format( + self.low_orig, self.high_orig, self.decimals, self.prior, self.transform_) + + def __contains__(self, point): + if isinstance(point, list): + point = np.array(point) + return self.low_orig <= point <= self.high_orig + + def transform(self, Xt): + aa = [int(x * pow(10, self.decimals)) for x in Xt] + return super().transform(aa) + + def inverse_transform(self, Xt): + res = super().inverse_transform(Xt) + return [round(x * pow(0.1, self.decimals), self.decimals) for x in res] diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 709179997..9a7fc4ba4 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -10,6 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): from skopt.space import Integer, Real, Categorical + from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.exceptions import OperationalException @@ -168,22 +169,13 @@ class DecimalParameter(RealParameter): super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) - def get_space(self, name: str) -> 'Integer': + def get_space(self, name: str) -> 'SKDecimal': """ Create skopt optimization space. :param name: A name of parameter field. """ - low = int(self.opt_range[0] * pow(10, self._decimals)) - high = int(self.opt_range[1] * pow(10, self._decimals)) - return Integer(low, high, name=name, **self._space_params) - - def _set_value(self, value: int): - """ - Update current value. Used by hyperopt functions for the purpose where optimization and - value spaces differ. - :param value: An integer value. - """ - self.value = round(value * pow(0.1, self._decimals), self._decimals) + return SKDecimal(*self.opt_range, decimals=self._decimals, name=name, + **self._space_params) class CategoricalParameter(BaseParameter): From fedff1a75ac7de025449c9fecd884cb8c17f80c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 22:10:20 +0200 Subject: [PATCH 0169/1386] Fix failing test --- tests/strategy/test_interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 3bfa691b4..0ee80e0c5 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -596,8 +596,6 @@ def test_hyperopt_parameters(): fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') assert isinstance(fltpar.get_space(''), Integer) assert fltpar.value == 1 - fltpar._set_value(2222) - assert fltpar.value == 2.222 catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') From 34e47db18d4cd8bd44b1a8cd125a45c43c115dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 22:15:24 +0200 Subject: [PATCH 0170/1386] Test SKDecimal space --- freqtrade/optimize/decimalspace.py | 1 - tests/optimize/test_hyperopt.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/decimalspace.py index fbc6a7af6..f5370b6d6 100644 --- a/freqtrade/optimize/decimalspace.py +++ b/freqtrade/optimize/decimalspace.py @@ -1,5 +1,4 @@ import numpy as np - from skopt.space import Integer diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c13da0d76..129fe53d9 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,6 +15,7 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException +from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -1104,3 +1105,20 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) hyperopt.start() + + +def test_SKDecimal(): + space = SKDecimal(1, 2, decimals=2) + assert 1.5 in space + assert 2.5 not in space + assert space.low == 100 + assert space.high == 200 + + assert space.inverse_transform([200]) == [2.0] + assert space.inverse_transform([100]) == [1.0] + assert space.inverse_transform([150, 160]) == [1.5, 1.6] + + assert space.transform([1.5]) == [150] + assert space.transform([2.0]) == [200] + assert space.transform([1.0]) == [100] + assert space.transform([1.5, 1.6]) == [150, 160] From ea4b5d675df701d9cb0d6b36ada7cd9023408c97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 07:12:31 +0200 Subject: [PATCH 0171/1386] Don't explode low/high, but use explicit parameters --- freqtrade/strategy/hyper.py | 48 ++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 9a7fc4ba4..c72beba55 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -25,9 +25,8 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any - opt_range: Sequence[Any] - def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, + def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. @@ -44,7 +43,6 @@ class BaseParameter(ABC): self.category = space self._space_params = kwargs self.value = default - self.opt_range = opt_range self.optimize = optimize self.load = load @@ -69,7 +67,6 @@ class BaseParameter(ABC): class IntParameter(BaseParameter): default: int value: int - opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -90,10 +87,12 @@ class IntParameter(BaseParameter): if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException('IntParameter space must be [low, high]') - opt_range = low + self.low, self.high = low else: - opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Integer': @@ -101,13 +100,12 @@ class IntParameter(BaseParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return Integer(*self.opt_range, name=name, **self._space_params) + return Integer(low=self.low, high=self.high, name=name, **self._space_params) class RealParameter(BaseParameter): default: float value: float - opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, space: Optional[str] = None, optimize: bool = True, @@ -129,10 +127,11 @@ class RealParameter(BaseParameter): if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - opt_range = low + self.low, self.high = low else: - opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + self.low = low + self.high = high + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Real': @@ -140,13 +139,12 @@ class RealParameter(BaseParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return Real(*self.opt_range, name=name, **self._space_params) + return Real(low=self.low, high=self.high, name=name, **self._space_params) -class DecimalParameter(RealParameter): +class DecimalParameter(BaseParameter): default: float value: float - opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, decimals: int = 3, space: Optional[str] = None, @@ -162,11 +160,22 @@ class DecimalParameter(RealParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param optimize: Include parameter in hyperopt optimizations. :param load: Load parameter value from {space}_params. - :param kwargs: Extra parameters to skopt.space.Real. + :param kwargs: Extra parameters to skopt.space.Integer. """ self._decimals = decimals default = round(default, self._decimals) - super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, + + if high is not None and isinstance(low, Sequence): + raise OperationalException(f'{self.__class__.__name__} space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') + self.low, self.high = low + else: + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'SKDecimal': @@ -174,7 +183,7 @@ class DecimalParameter(RealParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return SKDecimal(*self.opt_range, decimals=self._decimals, name=name, + return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name, **self._space_params) @@ -200,7 +209,8 @@ class CategoricalParameter(BaseParameter): if len(categories) < 2: raise OperationalException( 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, optimize=optimize, + self.opt_range = categories + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Categorical': From 83fbaf16c84d6395048e62956adf6509a8a70939 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 07:57:02 +0200 Subject: [PATCH 0172/1386] Extract numeric param validation and explosion --- freqtrade/strategy/hyper.py | 76 ++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index c72beba55..35000d916 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -64,7 +64,43 @@ class BaseParameter(ABC): self.value = value -class IntParameter(BaseParameter): +class NumericParameter(BaseParameter): + """ Internal parameter used for Numeric purposes """ + float_or_int = Union[int, float] + default: float_or_int + value: float_or_int + + def __init__(self, low: Union[float_or_int, Sequence[float_or_int]], + high: Optional[float_or_int] = None, *, default: float_or_int, + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): + """ + Initialize hyperopt-optimizable numeric parameter. + Cannot be instantiated, but provides the validation for other numeric parameters + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none of entire range is passed first parameter. + :param default: A default value. + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. + :param kwargs: Extra parameters to skopt.space.*. + """ + if high is not None and isinstance(low, Sequence): + raise OperationalException(f'{self.__class__.__name__} space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') + self.low, self.high = low + else: + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, + load=load, **kwargs) + + +class IntParameter(NumericParameter): default: int value: int @@ -82,17 +118,8 @@ class IntParameter(BaseParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Integer. """ - if high is not None and isinstance(low, Sequence): - raise OperationalException('IntParameter space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException('IntParameter space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Integer': @@ -103,7 +130,7 @@ class IntParameter(BaseParameter): return Integer(low=self.low, high=self.high, name=name, **self._space_params) -class RealParameter(BaseParameter): +class RealParameter(NumericParameter): default: float value: float @@ -122,16 +149,7 @@ class RealParameter(BaseParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Real. """ - if high is not None and isinstance(low, Sequence): - raise OperationalException(f'{self.__class__.__name__} space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Real': @@ -142,7 +160,7 @@ class RealParameter(BaseParameter): return Real(low=self.low, high=self.high, name=name, **self._space_params) -class DecimalParameter(BaseParameter): +class DecimalParameter(NumericParameter): default: float value: float @@ -165,17 +183,7 @@ class DecimalParameter(BaseParameter): self._decimals = decimals default = round(default, self._decimals) - if high is not None and isinstance(low, Sequence): - raise OperationalException(f'{self.__class__.__name__} space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'SKDecimal': From 9804e201146762f96752e18f41304b37bf47804a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 09:53:48 +0200 Subject: [PATCH 0173/1386] Don't use _set_value for autoOpt-Spaces --- freqtrade/optimize/hyperopt_auto.py | 4 ++-- freqtrade/strategy/hyper.py | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index c4d6f1581..f86204406 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -27,7 +27,7 @@ class HyperOptAuto(IHyperOpt): for attr_name, attr in self.strategy.enumerate_parameters('buy'): if attr.optimize: # noinspection PyProtectedMember - attr._set_value(params[attr_name]) + attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -37,7 +37,7 @@ class HyperOptAuto(IHyperOpt): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: # noinspection PyProtectedMember - attr._set_value(params[attr_name]) + attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) return populate_sell_trend diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 35000d916..3fedda974 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -50,19 +50,11 @@ class BaseParameter(ABC): return f'{self.__class__.__name__}({self.value})' @abstractmethod - def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: + def get_space(self, name: str) -> Union['Integer', 'Real', 'SKDecimal', 'Categorical']: """ Get-space - will be used by Hyperopt to get the hyperopt Space """ - def _set_value(self, value: Any): - """ - Update current value. Used by hyperopt functions for the purpose where optimization and - value spaces differ. - :param value: A numerical value. - """ - self.value = value - class NumericParameter(BaseParameter): """ Internal parameter used for Numeric purposes """ From ebbe47f38d884b139610d60db09708b32dcb6b71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 13:36:16 +0200 Subject: [PATCH 0174/1386] Simplify fiat convert and fix USD coingecko problem --- freqtrade/rpc/fiat_convert.py | 103 +++++++-------------------------- tests/rpc/test_fiat_convert.py | 78 +++---------------------- 2 files changed, 30 insertions(+), 151 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 4e26432d4..380070deb 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -4,9 +4,9 @@ e.g BTC to USD """ import logging -import time -from typing import Dict, List +from typing import Dict +from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI from freqtrade.constants import SUPPORTED_FIAT @@ -15,51 +15,6 @@ from freqtrade.constants import SUPPORTED_FIAT logger = logging.getLogger(__name__) -class CryptoFiat: - """ - Object to describe what is the price of Crypto-currency in a FIAT - """ - # Constants - CACHE_DURATION = 6 * 60 * 60 # 6 hours - - def __init__(self, crypto_symbol: str, fiat_symbol: str, price: float) -> None: - """ - Create an object that will contains the price for a crypto-currency in fiat - :param crypto_symbol: Crypto-currency you want to convert (e.g BTC) - :param fiat_symbol: FIAT currency you want to convert to (e.g USD) - :param price: Price in FIAT - """ - - # Public attributes - self.crypto_symbol = None - self.fiat_symbol = None - self.price = 0.0 - - # Private attributes - self._expiration = 0.0 - - self.crypto_symbol = crypto_symbol.lower() - self.fiat_symbol = fiat_symbol.lower() - self.set_price(price=price) - - def set_price(self, price: float) -> None: - """ - Set the price of the Crypto-currency in FIAT and set the expiration time - :param price: Price of the current Crypto currency in the fiat - :return: None - """ - self.price = price - self._expiration = time.time() + self.CACHE_DURATION - - def is_expired(self) -> bool: - """ - Return if the current price is still valid or needs to be refreshed - :return: bool, true the price is expired and needs to be refreshed, false the price is - still valid - """ - return self._expiration - time.time() <= 0 - - class CryptoToFiatConverter: """ Main class to initiate Crypto to FIAT. @@ -84,7 +39,9 @@ class CryptoToFiatConverter: return CryptoToFiatConverter.__instance def __init__(self) -> None: - self._pairs: List[CryptoFiat] = [] + # Timeout: 6h + self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60) + self._load_cryptomap() def _load_cryptomap(self) -> None: @@ -118,49 +75,31 @@ class CryptoToFiatConverter: """ crypto_symbol = crypto_symbol.lower() fiat_symbol = fiat_symbol.lower() + inverse = False + if crypto_symbol == 'usd': + # usd corresponds to "uniswap-state-dollar" for coingecko. + # We'll therefore need to "swap" the currencies + logger.info(f"reversing Rates {crypto_symbol}, {fiat_symbol}") + crypto_symbol = fiat_symbol + fiat_symbol = 'usd' + inverse = True + + symbol = f"{crypto_symbol}/{fiat_symbol}" # Check if the fiat convertion you want is supported if not self._is_supported_fiat(fiat=fiat_symbol): raise ValueError(f'The fiat {fiat_symbol} is not supported.') - # Get the pair that interest us and return the price in fiat - for pair in self._pairs: - if pair.crypto_symbol == crypto_symbol and pair.fiat_symbol == fiat_symbol: - # If the price is expired we refresh it, avoid to call the API all the time - if pair.is_expired(): - pair.set_price( - price=self._find_price( - crypto_symbol=pair.crypto_symbol, - fiat_symbol=pair.fiat_symbol - ) - ) + price = self._pair_price.get(symbol, None) - # return the last price we have for this pair - return pair.price - - # The pair does not exist, so we create it and return the price - return self._add_pair( - crypto_symbol=crypto_symbol, - fiat_symbol=fiat_symbol, - price=self._find_price( + if not price: + price = self._find_price( crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol ) - ) - - def _add_pair(self, crypto_symbol: str, fiat_symbol: str, price: float) -> float: - """ - :param crypto_symbol: Crypto-currency you want to convert (e.g BTC) - :param fiat_symbol: FIAT currency you want to convert to (e.g USD) - :return: price in FIAT - """ - self._pairs.append( - CryptoFiat( - crypto_symbol=crypto_symbol, - fiat_symbol=fiat_symbol, - price=price - ) - ) + if inverse and price != 0.0: + price = 1 / price + self._pair_price[symbol] = price return price diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index ed21bc516..2d43addff 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -1,44 +1,15 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, # pragma pylint: disable=protected-access, C0103 -import time from unittest.mock import MagicMock import pytest from requests.exceptions import RequestException -from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter +from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from tests.conftest import log_has, log_has_re -def test_pair_convertion_object(): - pair_convertion = CryptoFiat( - crypto_symbol='btc', - fiat_symbol='usd', - price=12345.0 - ) - - # Check the cache duration is 6 hours - assert pair_convertion.CACHE_DURATION == 6 * 60 * 60 - - # Check a regular usage - assert pair_convertion.crypto_symbol == 'btc' - assert pair_convertion.fiat_symbol == 'usd' - assert pair_convertion.price == 12345.0 - assert pair_convertion.is_expired() is False - - # Update the expiration time (- 2 hours) and check the behavior - pair_convertion._expiration = time.time() - 2 * 60 * 60 - assert pair_convertion.is_expired() is True - - # Check set price behaviour - time_reference = time.time() + pair_convertion.CACHE_DURATION - pair_convertion.set_price(price=30000.123) - assert pair_convertion.is_expired() is False - assert pair_convertion._expiration >= time_reference - assert pair_convertion.price == 30000.123 - - def test_fiat_convert_is_supported(mocker): fiat_convert = CryptoToFiatConverter() assert fiat_convert._is_supported_fiat(fiat='USD') is True @@ -47,28 +18,6 @@ def test_fiat_convert_is_supported(mocker): assert fiat_convert._is_supported_fiat(fiat='ABC') is False -def test_fiat_convert_add_pair(mocker): - - fiat_convert = CryptoToFiatConverter() - - pair_len = len(fiat_convert._pairs) - assert pair_len == 0 - - fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0) - pair_len = len(fiat_convert._pairs) - assert pair_len == 1 - assert fiat_convert._pairs[0].crypto_symbol == 'btc' - assert fiat_convert._pairs[0].fiat_symbol == 'usd' - assert fiat_convert._pairs[0].price == 12345.0 - - fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2) - pair_len = len(fiat_convert._pairs) - assert pair_len == 2 - assert fiat_convert._pairs[1].crypto_symbol == 'btc' - assert fiat_convert._pairs[1].fiat_symbol == 'eur' - assert fiat_convert._pairs[1].price == 13000.2 - - def test_fiat_convert_find_price(mocker): fiat_convert = CryptoToFiatConverter() @@ -95,8 +44,8 @@ def test_fiat_convert_unsupported_crypto(mocker, caplog): def test_fiat_convert_get_price(mocker): - mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', - return_value=28000.0) + find_price = mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', + return_value=28000.0) fiat_convert = CryptoToFiatConverter() @@ -104,26 +53,17 @@ def test_fiat_convert_get_price(mocker): fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='US Dollar') # Check the value return by the method - pair_len = len(fiat_convert._pairs) + pair_len = len(fiat_convert._pair_price) assert pair_len == 0 assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0 - assert fiat_convert._pairs[0].crypto_symbol == 'btc' - assert fiat_convert._pairs[0].fiat_symbol == 'usd' - assert fiat_convert._pairs[0].price == 28000.0 - assert fiat_convert._pairs[0]._expiration != 0 - assert len(fiat_convert._pairs) == 1 + assert fiat_convert._pair_price['btc/usd'] == 28000.0 + assert len(fiat_convert._pair_price) == 1 + assert find_price.call_count == 1 # Verify the cached is used - fiat_convert._pairs[0].price = 9867.543 - expiration = fiat_convert._pairs[0]._expiration + fiat_convert._pair_price['btc/usd'] = 9867.543 assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 9867.543 - assert fiat_convert._pairs[0]._expiration == expiration - - # Verify the cache expiration - expiration = time.time() - 2 * 60 * 60 - fiat_convert._pairs[0]._expiration = expiration - assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0 - assert fiat_convert._pairs[0]._expiration is not expiration + assert find_price.call_count == 1 def test_fiat_convert_same_currencies(mocker): From 37c2e037f11da94b7c55a567d30c6184830a3148 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 13:50:56 +0200 Subject: [PATCH 0175/1386] Rename dry_run_order to create_dry_run_order --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/exchange.py | 8 ++++---- freqtrade/exchange/ftx.py | 2 +- freqtrade/exchange/kraken.py | 2 +- tests/exchange/test_exchange.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 26ec30a8a..0bcfa5e17 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -52,7 +52,7 @@ class Binance(Exchange): 'In stoploss limit order, stop price should be more than limit price') if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 37d92e253..7edace13c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -543,8 +543,8 @@ class Exchange: # See also #2575 at github. return max(min_stake_amounts) * amount_reserve_percent - def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict[str, Any]: + def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, + rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order = { @@ -618,7 +618,7 @@ class Exchange: rate: float, time_in_force: str) -> Dict: if self._config['dry_run']: - dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) + dry_order = self.create_dry_run_order(pair, ordertype, "buy", amount, rate) return dry_order params = self._params.copy() @@ -631,7 +631,7 @@ class Exchange: rate: float, time_in_force: str = 'gtc') -> Dict: if self._config['dry_run']: - dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) + dry_order = self.create_dry_run_order(pair, ordertype, "sell", amount, rate) return dry_order params = self._params.copy() diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f05490cbb..6312759b9 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -53,7 +53,7 @@ class Ftx(Exchange): stop_price = self.price_to_precision(pair, stop_price) if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 724b11189..786f1b592 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -92,7 +92,7 @@ class Kraken(Exchange): stop_price = self.price_to_precision(pair, stop_price) if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3439c7a09..202f1885f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -931,11 +931,11 @@ def test_exchange_has(default_conf, mocker): ("sell") ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_dry_run_order(default_conf, mocker, side, exchange_name): +def test_create_dry_run_order(default_conf, mocker, side, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - order = exchange.dry_run_order( + order = exchange.create_dry_run_order( pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) assert 'id' in order assert f'dry_run_{side}_' in order["id"] From 14e857423528b87e0cbfd46f1e3ad2264cda8ccd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:13:00 +0200 Subject: [PATCH 0176/1386] fetch_balance is never called in dry-run --- freqtrade/exchange/exchange.py | 4 ---- tests/exchange/test_exchange.py | 15 --------------- 2 files changed, 19 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7edace13c..3224255d0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -662,8 +662,6 @@ class Exchange: @retrier def get_balance(self, currency: str) -> float: - if self._config['dry_run']: - return self._config['dry_run_wallet'] # ccxt exception is already handled by get_balances balances = self.get_balances() @@ -675,8 +673,6 @@ class Exchange: @retrier def get_balances(self) -> dict: - if self._config['dry_run']: - return {} try: balances = self._api.fetch_balance() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 202f1885f..4ceba6eba 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1245,14 +1245,6 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): assert "timeInForce" not in api_mock.create_order.call_args[0][5] -def test_get_balance_dry_run(default_conf, mocker): - default_conf['dry_run'] = True - default_conf['dry_run_wallet'] = 999.9 - - exchange = get_patched_exchange(mocker, default_conf) - assert exchange.get_balance(currency='BTC') == 999.9 - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balance_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() @@ -1276,13 +1268,6 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): exchange.get_balance(currency='BTC') -@pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_balances_dry_run(default_conf, mocker, exchange_name): - default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - assert exchange.get_balances() == {} - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balances_prod(default_conf, mocker, exchange_name): balance_item = { From 96a5b6555dd6a2c4f7fc62112d618f7c1d928843 Mon Sep 17 00:00:00 2001 From: gbojen Date: Sat, 10 Apr 2021 14:31:12 +0200 Subject: [PATCH 0177/1386] fix documentation inconsistency fixes freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index d57757bbd..8688494cc 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -174,7 +174,7 @@ This filter removes pairs if the average volatility over a `lookback_days` days This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. In the below example: -If the volatility over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. +If the volatility over the last 10 days is not in the range of 0.05-0.50, remove the pair from the whitelist. The filter is applied every 24h. ```json "pairlists": [ @@ -190,7 +190,7 @@ If the volatility over the last 10 days is not in the range of 0.20-0.30, remove ### Full example of Pairlist Handlers -The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. ```json "exchange": { From 579e68f31e122a3ebbd897371642a7ee299749c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:37:09 +0200 Subject: [PATCH 0178/1386] Reduce log verbosity when buying --- freqtrade/freqtradebot.py | 11 ++++------- tests/test_freqtradebot.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a701e8db9..1ebf28ebd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -410,9 +410,7 @@ class FreqtradeBot(LoggingMixin): bid_strategy = self.config.get('bid_strategy', {}) if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False): - logger.info( - f"Getting price from order book {bid_strategy['price_side'].capitalize()} side." - ) + order_book_top = bid_strategy.get('order_book_top', 1) order_book = self.exchange.fetch_l2_order_book(pair, order_book_top) logger.debug('order_book %s', order_book) @@ -425,7 +423,8 @@ class FreqtradeBot(LoggingMixin): f"Orderbook: {order_book}" ) raise PricingError from e - logger.info(f'...top {order_book_top} order book buy rate {rate_from_l2:.8f}') + logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side " + f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}") used_rate = rate_from_l2 else: logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price") @@ -479,19 +478,17 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False - logger.info(f"Buy signal found: about create a new trade with stake_amount: " + logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): if self._check_depth_of_market_buy(pair, bid_check_dom): - logger.info(f'Executing Buy for {pair}.') return self.execute_buy(pair, stake_amount) else: return False - logger.info(f'Executing Buy for {pair}') return self.execute_buy(pair, stake_amount) else: return False diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c93f8b858..a7b9bb103 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -685,7 +685,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.amount == 91.07468123 assert log_has( - 'Buy signal found: about create a new trade with stake_amount: 0.001 ...', caplog + 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', caplog ) From 4820b4b314096fc2529ad67f5d2dd2a8ccd431b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:52:34 +0200 Subject: [PATCH 0179/1386] Fix test failure --- tests/test_freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a7b9bb103..c91015766 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -685,7 +685,8 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.amount == 91.07468123 assert log_has( - 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', caplog + 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', + caplog ) From aaf9872ef37b5959ce1489da8470ce1dd534c2ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 19:53:00 +0200 Subject: [PATCH 0180/1386] Simplify webserver test --- tests/rpc/test_rpc_apiserver.py | 61 +++++++++++++-------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d113a8802..e72749715 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -416,10 +416,10 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json()["max"] == 1 # Create some test data - ftbot.enter_positions() + create_mock_trades(fee) rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) - assert rc.json()["current"] == 1 + assert rc.json()["current"] == 4 assert rc.json()["max"] == 1 ftbot.config['max_open_trades'] = float('inf') @@ -612,7 +612,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") -def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): +def test_api_profit(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) mocker.patch.multiple( @@ -627,48 +627,33 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert_response(rc, 200) assert rc.json()['trade_count'] == 0 - ftbot.enter_positions() - trade = Trade.query.first() - + create_mock_trades(fee) # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - rc = client_get(client, f"{BASE_URI}/profit") - assert_response(rc, 200) - # One open trade - assert rc.json()['trade_count'] == 1 - assert rc.json()['best_pair'] == '' - assert rc.json()['best_rate'] == 0 - - trade = Trade.query.first() - trade.update(limit_sell_order) - - trade.close_date = datetime.utcnow() - trade.is_open = False rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc) assert rc.json() == {'avg_duration': ANY, - 'best_pair': 'ETH/BTC', - 'best_rate': 6.2, - 'first_trade_date': 'just now', + 'best_pair': 'XRP/BTC', + 'best_rate': 1.0, + 'first_trade_date': ANY, 'first_trade_timestamp': ANY, - 'latest_trade_date': 'just now', + 'latest_trade_date': '5 minutes ago', 'latest_trade_timestamp': ANY, - 'profit_all_coin': 6.217e-05, - 'profit_all_fiat': 0.76748865, - 'profit_all_percent_mean': 6.2, - 'profit_all_ratio_mean': 0.06201058, - 'profit_all_percent_sum': 6.2, - 'profit_all_ratio_sum': 0.06201058, - 'profit_closed_coin': 6.217e-05, - 'profit_closed_fiat': 0.76748865, - 'profit_closed_ratio_mean': 0.06201058, - 'profit_closed_percent_mean': 6.2, - 'profit_closed_ratio_sum': 0.06201058, - 'profit_closed_percent_sum': 6.2, - 'trade_count': 1, - 'closed_trade_count': 1, - 'winning_trades': 1, + 'profit_all_coin': -44.0631579, + 'profit_all_fiat': -543959.6842755, + 'profit_all_percent_mean': -66.41, + 'profit_all_ratio_mean': -0.6641100666666667, + 'profit_all_percent_sum': -398.47, + 'profit_all_ratio_sum': -3.9846604, + 'profit_closed_coin': 0.00073913, + 'profit_closed_fiat': 9.124559849999999, + 'profit_closed_ratio_mean': 0.0075, + 'profit_closed_percent_mean': 0.75, + 'profit_closed_ratio_sum': 0.015, + 'profit_closed_percent_sum': 1.5, + 'trade_count': 6, + 'closed_trade_count': 2, + 'winning_trades': 2, 'losing_trades': 0, } From 906c4e64d335e6d401eac2c85d3fa0f0b2319f46 Mon Sep 17 00:00:00 2001 From: Ugur Cem Ozturk Date: Sun, 11 Apr 2021 15:38:08 +0300 Subject: [PATCH 0181/1386] chore(readme): Fix markdown of docker manual Link to docker-compose was pointing to the one from develop branch. It's changed as with the stable docker-compose. --- docs/docker_quickstart.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index ca0515281..b133e33f0 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -22,10 +22,10 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co ### Docker quick start -Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. +Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory. -=== "PC/MAC/Linux" - ``` bash +#### PC/MAC/Linux + mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -39,10 +39,10 @@ Create a new directory and place the [docker-compose file](https://github.com/fr # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` + + +#### RaspberryPi -=== "RaspberryPi" - ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -56,7 +56,6 @@ Create a new directory and place the [docker-compose file](https://github.com/fr # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. @@ -68,12 +67,11 @@ Create a new directory and place the [docker-compose file](https://github.com/fr The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. -!!! Question "How to edit the bot configuration?" - You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. +### Question: "How to edit the bot configuration?" +You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. +You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. - You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. - -#### Adding a custom strategy +##### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` @@ -81,7 +79,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a The `SampleStrategy` is run by default. -!!! Warning "`SampleStrategy` is just a demo!" +#### Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest your strategy and use dry-run for some time before risking real money! You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). @@ -92,7 +90,7 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` -!!! Warning "Default configuration" +#### Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. #### Monitoring the bot @@ -122,7 +120,7 @@ docker-compose up -d This will first pull the latest image, and will then restart the container with the just pulled version. -!!! Warning "Check the Changelog" +#### Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. ### Editing the docker-compose file @@ -131,7 +129,7 @@ Advanced users may edit the docker-compose file further to include all possible All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. -!!! Note "`docker-compose run --rm`" +#### Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose From 1b925ec4a9eff02f47be05263b472ae298c36631 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:11 +0000 Subject: [PATCH 0182/1386] Bump sqlalchemy from 1.4.5 to 1.4.7 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.5 to 1.4.7. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1877624cf..8bcb9608c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.46.38 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.5 +SQLAlchemy==1.4.7 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 53bbb2b42c9e808252f8bdc7dd0a45d76c29f4bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:47 +0000 Subject: [PATCH 0183/1386] Bump ccxt from 1.46.38 to 1.47.47 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.46.38 to 1.47.47. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.46.38...1.47.47) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1877624cf..8705ef8e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.46.38 +ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From c19ebc0157cd7157099182e101aed8006ba463fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:58 +0000 Subject: [PATCH 0184/1386] Bump mkdocs-material from 7.1.0 to 7.1.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.0 to 7.1.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.0...7.1.1) 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 3dbaea111..cfd63d1d0 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.0 +mkdocs-material==7.1.1 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From f1ac6853fc64270a32930e991d6d809c9576bc55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Apr 2021 11:11:53 +0200 Subject: [PATCH 0185/1386] Fix discord invite link --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index c890caf1a..7233a92fe 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -156,7 +156,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/MA9v74M). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: From d4dc05980c8b155dd8e7a1673387fdc20f505903 Mon Sep 17 00:00:00 2001 From: Chris van de Steeg Date: Mon, 12 Apr 2021 16:01:46 +0200 Subject: [PATCH 0186/1386] Update ftx.py Stoploss price should be set as param instead of passing it as price according to ccxt --- freqtrade/exchange/ftx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6312759b9..fdfd5a674 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -63,10 +63,11 @@ class Ftx(Exchange): # set orderPrice to place limit order, otherwise it's a market order params['orderPrice'] = limit_rate + params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', - amount=amount, price=stop_price, params=params) + amount=amount, params=params) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) return order From 1194d0c0f4ca3e6f7c595345849e839b0ff30a43 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Apr 2021 20:03:37 +0200 Subject: [PATCH 0187/1386] Update brew before installing packages --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e15a5a89..102e6ed78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,6 +148,7 @@ jobs: - name: Installation - macOS run: | + brew update brew install hdf5 c-blosc python -m pip install --upgrade pip export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH From 9a58a8534773393181012bc93930f1b138530c9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:17:11 +0200 Subject: [PATCH 0188/1386] Don't export "hum" date versions for trade objects. They are not used and have a rather high performance penalty due to using arrow.get --- freqtrade/persistence/models.py | 3 --- freqtrade/rpc/api_server/api_schemas.py | 2 -- freqtrade/rpc/telegram.py | 1 + tests/rpc/test_rpc.py | 4 ---- tests/rpc/test_rpc_apiserver.py | 4 ---- tests/rpc/test_rpc_telegram.py | 2 -- tests/test_persistence.py | 4 ---- 7 files changed, 1 insertion(+), 19 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a22e75e1e..8b4aa325a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -294,15 +294,12 @@ class LocalTrade(): 'fee_close_cost': self.fee_close_cost, 'fee_close_currency': self.fee_close_currency, - 'open_date_hum': arrow.get(self.open_date).humanize(), 'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, 'open_trade_value': round(self.open_trade_value, 8), - 'close_date_hum': (arrow.get(self.close_date).humanize() - if self.close_date else None), 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None), 'close_timestamp': int(self.close_date.replace( diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index eaca477d7..41de0134c 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -151,13 +151,11 @@ class TradeSchema(BaseModel): fee_close: Optional[float] fee_close_cost: Optional[float] fee_close_currency: Optional[str] - open_date_hum: str open_date: str open_timestamp: int open_rate: float open_rate_requested: Optional[float] open_trade_value: float - close_date_hum: Optional[str] close_date: Optional[str] close_timestamp: Optional[int] close_rate: Optional[float] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 17ddd1c91..a8c629149 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -294,6 +294,7 @@ class Telegram(RPCHandler): messages = [] for r in results: + r['open_date_hum'] = arrow.get(r['open_date']).humanize() lines = [ "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 64918ed47..a97f6b65e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -53,7 +53,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, @@ -73,7 +72,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'open_rate': 1.098e-05, 'close_rate': None, @@ -121,7 +119,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, @@ -141,7 +138,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'open_rate': 1.098e-05, 'close_rate': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e72749715..2b6d96c61 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -755,7 +755,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'amount_requested': 123.0, 'base_currency': 'BTC', 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'close_profit': None, 'close_profit_pct': None, @@ -770,7 +769,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'open_order': None, 'open_rate': 0.123, @@ -922,11 +920,9 @@ def test_api_forcebuy(botclient, mocker, fee): 'amount_requested': 1, 'trade_id': 22, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'close_rate': 0.265441, 'open_date': ANY, - 'open_date_hum': 'just now', 'open_timestamp': ANY, 'open_rate': 0.245441, 'pair': 'ETH/ETH', diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 27babb1b7..34bf057cb 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -177,9 +177,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': arrow.utcnow(), - 'open_date_hum': arrow.utcnow().humanize, 'close_date': None, - 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3336e4e66..0a3d6858d 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -799,11 +799,9 @@ def test_to_json(default_conf, fee): assert result == {'trade_id': None, 'pair': 'ETH/BTC', 'is_open': None, - 'open_date_hum': '2 hours ago', 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), 'open_order_id': 'dry_run_buy_12345', - 'close_date_hum': None, 'close_date': None, 'close_timestamp': None, 'open_rate': 0.123, @@ -865,10 +863,8 @@ def test_to_json(default_conf, fee): assert result == {'trade_id': None, 'pair': 'XRP/BTC', - 'open_date_hum': '2 hours ago', 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'close_date_hum': 'an hour ago', 'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"), 'close_timestamp': int(trade.close_date.timestamp() * 1000), 'open_rate': 0.123, From 4b902d6eb8e0e17e94365bd4de2b6c8c256d8b24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:23:11 +0200 Subject: [PATCH 0189/1386] Don't use response-model on trades endpoint for now --- freqtrade/rpc/api_server/api_v1.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index b983402e9..663cc9ff4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -17,8 +17,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, - StrategyResponse, TradeResponse, Version, - WhitelistResponse) + StrategyResponse, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -83,7 +82,9 @@ def status(rpc: RPC = Depends(get_rpc)): return [] -@router.get('/trades', response_model=TradeResponse, tags=['info', 'trading']) +# Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s) +# on big databases. Correct response model: response_model=TradeResponse, +@router.get('/trades', tags=['info', 'trading']) def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) From 9b23be402114cdde6f4ed7d2468c3b90595a931d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:49:53 +0200 Subject: [PATCH 0190/1386] Return a copy from `current_whitelist` this avoids manipulating of the pair whitelist from within a strategy --- freqtrade/data/dataprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index a035b7c3b..b4dea0743 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -170,6 +170,6 @@ class DataProvider: """ if self._pairlists: - return self._pairlists.whitelist + return self._pairlists.whitelist.copy() else: raise OperationalException("Dataprovider was not initialized with a pairlist provider.") From f1cf56cc4277ec5c9f7184a58f29b9169200d984 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:57:21 +0200 Subject: [PATCH 0191/1386] Update current_whitelist test --- tests/data/test_dataprovider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index ee2e551b6..6b33fa7f2 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -214,8 +214,8 @@ def test_current_whitelist(mocker, default_conf, tickers): pairlist.refresh_pairlist() assert dp.current_whitelist() == pairlist._whitelist - # The identity of the 2 lists should be identical - assert dp.current_whitelist() is pairlist._whitelist + # The identity of the 2 lists should not be identical, but a copy + assert dp.current_whitelist() is not pairlist._whitelist with pytest.raises(OperationalException): dp = DataProvider(default_conf, exchange) From 99e7ee12731533d91b2206e7f2df5008260ffb83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 08:26:26 +0200 Subject: [PATCH 0192/1386] Fix ftx stoploss creation test --- tests/exchange/test_ftx.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 17cfb26fa..494d86e56 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -39,8 +39,9 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 - assert api_mock.create_order.call_args_list[0][1]['price'] == 190 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 190 assert api_mock.create_order.call_count == 1 @@ -55,8 +56,8 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 - assert api_mock.create_order.call_args_list[0][1]['price'] == 220 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, @@ -69,9 +70,9 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 - assert api_mock.create_order.call_args_list[0][1]['price'] == 220 assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 # test exception handling with pytest.raises(DependencyException): From e0f2bb6160f9478688601a0ffda7b35cb26439ca Mon Sep 17 00:00:00 2001 From: wr0ngc0degen Date: Tue, 13 Apr 2021 11:44:07 +0200 Subject: [PATCH 0193/1386] update conda dependencies to make compatible with tables package - restrict python version in conda's environment.yml to fixed installation issues due to current incompatibility of tables package with python 3.9 --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 938b5b6b8..f58434c15 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,7 @@ channels: # - defaults dependencies: # 1/4 req main - - python>=3.7 + - python>=3.7,<3.9 - numpy - pandas - pip From 37c8fd6ad799ed0320147304304acdad3e18e0b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 11:55:03 +0200 Subject: [PATCH 0194/1386] Remove arrow from models.py --- freqtrade/persistence/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8b4aa325a..49d3e2d62 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -6,7 +6,6 @@ from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional -import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError @@ -160,8 +159,8 @@ class Order(_DECL_BASE): if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False if order.get('filled', 0) > 0: - self.order_filled_date = arrow.utcnow().datetime - self.order_update_date = arrow.utcnow().datetime + self.order_filled_date = datetime.now(timezone.utc) + self.order_update_date = datetime.now(timezone.utc) @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): From 638cd4e8f13b963b03beec0b311f520cf35f0185 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 12:04:22 +0200 Subject: [PATCH 0195/1386] Upgrade cleanup action to latest version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 102e6ed78..4169661c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,7 +301,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch - uses: rokroskar/workflow-run-cleanup-action@v0.2.2 + uses: rokroskar/workflow-run-cleanup-action@v0.3.2 if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From e4bb6b158294a0b6b36ac5b9be41f93c9bbedafe Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 12:28:07 +0200 Subject: [PATCH 0196/1386] Add kucoin exchange subclass Kucoin has some specific orderbook restrictions closes #4723 --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bittrex.py | 4 ---- freqtrade/exchange/exchange.py | 14 +++++++++++--- freqtrade/exchange/kucoin.py | 24 ++++++++++++++++++++++++ tests/exchange/test_ccxt_compat.py | 13 ++++++++++--- tests/exchange/test_exchange.py | 3 +++ 6 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 freqtrade/exchange/kucoin.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 8a5563623..889bb49c2 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -15,3 +15,4 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken +from freqtrade.exchange.kucoin import Kucoin diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index fd7d47668..69e2f2b8d 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -12,10 +12,6 @@ class Bittrex(Exchange): """ Bittrex exchange class. Contains adjustments needed for Freqtrade to work with this exchange. - - Please note that this exchange is not included in the list of exchanges - officially supported by the Freqtrade development team. So some features - may still not work as expected. """ _ft_has: Dict = { diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3224255d0..e52e0e0d0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -63,6 +63,7 @@ class Exchange: "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", "l2_limit_range": None, + "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) } _ft_has: Dict = {} @@ -1154,14 +1155,20 @@ class Exchange: return self.fetch_order(order_id, pair) @staticmethod - def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]): + def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]], + range_required: bool = True): """ Get next greater value in the list. Used by fetch_l2_order_book if the api only supports a limited range """ if not limit_range: return limit - return min([x for x in limit_range if limit <= x] + [max(limit_range)]) + + result = min([x for x in limit_range if limit <= x] + [max(limit_range)]) + if not range_required and limit > result: + # Range is not required - we can use None as parameter. + return None + return result @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: @@ -1171,7 +1178,8 @@ class Exchange: Returns a dict in the format {'asks': [price, volume], 'bids': [price, volume]} """ - limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range']) + limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'], + self._ft_has['l2_limit_range_required']) try: return self._api.fetch_l2_order_book(pair, limit1) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py new file mode 100644 index 000000000..22886a1d8 --- /dev/null +++ b/freqtrade/exchange/kucoin.py @@ -0,0 +1,24 @@ +""" Kucoin exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Kucoin(Exchange): + """ + Kucoin exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "l2_limit_range": [20, 100], + "l2_limit_range_required": False, + } diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 870e6cabd..dce10da84 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -36,7 +36,12 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'hasQuoteVolume': True, 'timeframe': '5m', - } + }, + 'kucoin': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + }, } @@ -100,14 +105,16 @@ class TestCCXTExchange(): assert 'asks' in l2 assert 'bids' in l2 l2_limit_range = exchange._ft_has['l2_limit_range'] + l2_limit_range_required = exchange._ft_has['l2_limit_range_required'] for val in [1, 2, 5, 25, 100]: l2 = exchange.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: assert len(l2['asks']) == val assert len(l2['bids']) == val else: - next_limit = exchange.get_next_limit_in_list(val, l2_limit_range) - if next_limit > 200: + next_limit = exchange.get_next_limit_in_list( + val, l2_limit_range, l2_limit_range_required) + if next_limit is None or next_limit > 200: # Large orderbook sizes can be a problem for some exchanges (bitrex ...) assert len(l2['asks']) > 200 assert len(l2['asks']) > 200 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4ceba6eba..db67d038c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1641,6 +1641,9 @@ def test_get_next_limit_in_list(): # Going over the limit ... assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000 assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000 + # Without required range + assert Exchange.get_next_limit_in_list(2000, limit_range, False) is None + assert Exchange.get_next_limit_in_list(15, limit_range, False) == 20 assert Exchange.get_next_limit_in_list(21, None) == 21 assert Exchange.get_next_limit_in_list(100, None) == 100 From 521e48c94a5e5b18ba409d965757c7b8f509f08f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 13:55:08 +0200 Subject: [PATCH 0197/1386] Add doc section for Kucoin part of #4723 --- docs/exchanges.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 1c5956088..662f2b908 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -100,6 +100,18 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll } ``` +## Kucoin + +Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: + +```json +"exchange": { + "name": "kucoin", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "password": "your_exchange_api_key_password", +``` + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. From 82d66410f74b47f25cb4af60b702cad6fdebcae8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 19:20:57 +0200 Subject: [PATCH 0198/1386] Fix /performance output if multiple messages are necessary closes #4726 --- 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 a8c629149..09b7b235c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -702,7 +702,7 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: - self._send_msg(output) + self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line From c2f35ce416cf8d6e6c5806dd1a3e624a124d3843 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 20:09:22 +0200 Subject: [PATCH 0199/1386] /balance should use cached tickers when possible --- freqtrade/exchange/exchange.py | 18 ++++++++++++++++-- freqtrade/rpc/rpc.py | 2 +- tests/exchange/test_exchange.py | 8 ++++++++ tests/rpc/test_rpc.py | 2 ++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e52e0e0d0..3627a07e4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt import ccxt.async_support as ccxt_async +from cachetools import TTLCache from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame @@ -84,6 +85,9 @@ class Exchange: # Timestamp of last markets refresh self._last_markets_refresh: int = 0 + # Cache for 10 minutes ... + self._fetch_tickers_cache = TTLCache(maxsize=1, ttl=60 * 10) + # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} @@ -693,9 +697,19 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_tickers(self) -> Dict: + def get_tickers(self, cached: bool = False) -> Dict: + """ + :param cached: Allow cached result + :return: fetch_tickers result + """ + if cached: + tickers = self._fetch_tickers_cache.get('fetch_tickers') + if tickers: + return tickers try: - return self._api.fetch_tickers() + tickers = self._api.fetch_tickers() + self._fetch_tickers_cache['fetch_tickers'] = tickers + return tickers except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 59758a573..88f8b36db 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -442,7 +442,7 @@ class RPC: output = [] total = 0.0 try: - tickers = self._freqtrade.exchange.get_tickers() + tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index db67d038c..76095be2d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1319,6 +1319,14 @@ def test_get_tickers(default_conf, mocker, exchange_name): assert tickers['ETH/BTC']['ask'] == 1 assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 + assert api_mock.fetch_tickers.call_count == 1 + + api_mock.fetch_tickers.reset_mock() + + # Cached ticker should not call api again + tickers2 = exchange.get_tickers(cached=True) + assert tickers2 == tickers + assert api_mock.fetch_tickers.call_count == 0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a97f6b65e..199845545 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -569,6 +569,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) + assert tickers.call_count == 1 + assert tickers.call_args.kwargs['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From c316531c491c995c62bda57bda177669b8d41690 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 21:54:06 +0200 Subject: [PATCH 0200/1386] make tests 3.7 compatible --- tests/exchange/test_exchange.py | 2 ++ tests/rpc/test_rpc.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 76095be2d..882cf6b5a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1327,6 +1327,8 @@ def test_get_tickers(default_conf, mocker, exchange_name): tickers2 = exchange.get_tickers(cached=True) assert tickers2 == tickers assert api_mock.fetch_tickers.call_count == 0 + tickers2 = exchange.get_tickers(cached=False) + assert api_mock.fetch_tickers.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 199845545..a548505a7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -570,7 +570,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert tickers.call_count == 1 - assert tickers.call_args.kwargs['cached'] is True + assert tickers.call_args[1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From ba38e398e42359293dd13f00b7b44615771f5fd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 22:17:42 +0200 Subject: [PATCH 0201/1386] Add type hint --- freqtrade/exchange/exchange.py | 2 +- tests/rpc/test_rpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3627a07e4..3958f6838 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -86,7 +86,7 @@ class Exchange: self._last_markets_refresh: int = 0 # Cache for 10 minutes ... - self._fetch_tickers_cache = TTLCache(maxsize=1, ttl=60 * 10) + self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a548505a7..63e09cbe1 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -570,7 +570,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert tickers.call_count == 1 - assert tickers.call_args[1]['cached'] is True + assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From 862df2b431021d43f8ab5fcba198cff07250d336 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 19:43:32 +0200 Subject: [PATCH 0202/1386] Add blacklist recommendation for kucoin closes #4738 --- docs/exchanges.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 662f2b908..8797ade8c 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -7,10 +7,10 @@ This page combines common gotchas and informations which are exchange-specific a !!! Tip "Stoploss on Exchange" Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. -### Blacklists +### Binance Blacklist For Binance, please add `"BNB/"` to your blacklist to avoid issues. -Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore. +Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. ### Binance sites @@ -112,6 +112,11 @@ Kucoin requries a passphrase for each api key, you will therefore need to add th "password": "your_exchange_api_key_password", ``` +### Kucoin Blacklists + +For Kucoin, please add `"KCS/"` to your blacklist to avoid issues. +Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. From e820814809f6bef69f1501f16fb797d841034dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 20:32:34 +0200 Subject: [PATCH 0203/1386] Default-stoploss-hyperopt should use decimal space, nto real --- freqtrade/optimize/hyperopt_interface.py | 3 ++- freqtrade/optimize/space/__init__.py | 4 ++++ freqtrade/optimize/{ => space}/decimalspace.py | 0 freqtrade/strategy/hyper.py | 2 +- freqtrade/templates/sample_hyperopt_advanced.py | 4 ++-- tests/optimize/test_hyperopt.py | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 freqtrade/optimize/space/__init__.py rename freqtrade/optimize/{ => space}/decimalspace.py (100%) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 633c8bdd5..fa28463e9 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -12,6 +12,7 @@ from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict +from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IStrategy @@ -167,7 +168,7 @@ class IHyperOpt(ABC): You may override it in your custom Hyperopt class. """ return [ - Real(-0.35, -0.02, name='stoploss'), + SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), ] def generate_trailing_params(self, params: Dict) -> Dict: diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py new file mode 100644 index 000000000..bbdac4ab9 --- /dev/null +++ b/freqtrade/optimize/space/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa: F401 +from skopt.space import Categorical, Dimension, Integer, Real + +from .decimalspace import SKDecimal diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/space/decimalspace.py similarity index 100% rename from freqtrade/optimize/decimalspace.py rename to freqtrade/optimize/space/decimalspace.py diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 3fedda974..16b576a73 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -10,7 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): from skopt.space import Integer, Real, Categorical - from freqtrade.optimize.decimalspace import SKDecimal + from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 7736570f7..32ba21716 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -7,7 +7,7 @@ from typing import Any, Callable, Dict, List import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -237,7 +237,7 @@ class AdvancedSampleHyperOpt(IHyperOpt): 'stoploss' optimization hyperspace. """ return [ - Real(-0.35, -0.02, name='stoploss'), + SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), ] @staticmethod diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 129fe53d9..59bc4aefb 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,10 +15,10 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException -from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, From 52c482cecfd3133a1e8f741f9b708eec55bd41f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 20:34:34 +0200 Subject: [PATCH 0204/1386] Convert trailing and roi defaults to skdecimal --- freqtrade/optimize/hyperopt_interface.py | 15 +++++++++------ freqtrade/templates/sample_hyperopt_advanced.py | 10 +++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index fa28463e9..1bb471b9c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -7,7 +7,7 @@ import math from abc import ABC from typing import Any, Callable, Dict, List -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes @@ -155,9 +155,12 @@ class IHyperOpt(ABC): Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), - Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'), - Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'), - Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'), + SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=5, + name='roi_p1'), + SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=5, + name='roi_p2'), + SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=5, + name='roi_p3'), ] def stoploss_space(self) -> List[Dimension]: @@ -198,14 +201,14 @@ class IHyperOpt(ABC): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.01, 0.35, name='trailing_stop_positive'), + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between # them. The value of the 'trailing_stop_positive_offset' is constructed in the # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. - Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), Categorical([True, False], name='trailing_only_offset_is_reached'), ] diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 32ba21716..cc13b6ba3 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -223,9 +223,9 @@ class AdvancedSampleHyperOpt(IHyperOpt): Integer(10, 120, name='roi_t1'), Integer(10, 60, name='roi_t2'), Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), + SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'), + SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'), + SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), ] @staticmethod @@ -256,14 +256,14 @@ class AdvancedSampleHyperOpt(IHyperOpt): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.01, 0.35, name='trailing_stop_positive'), + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between # them. The value of the 'trailing_stop_positive_offset' is constructed in the # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. - Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), Categorical([True, False], name='trailing_only_offset_is_reached'), ] From fa343b0484a5850ee9181a7a825ef9f3653d5d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:19:30 +0100 Subject: [PATCH 0205/1386] Fix get_min_pair_stake_amount formula --- freqtrade/exchange/exchange.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3958f6838..ea6bcb29f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -539,7 +539,9 @@ class Exchange: # reserve some percent defined in config (5% default) + stoploss amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += abs(stoploss) + amount_reserve_percent = ( + amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) From 885096f2b3830e14bf8b1678ad31e696775ddfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:22:52 +0100 Subject: [PATCH 0206/1386] Update tests for get_min_pair_stake_amount --- tests/exchange/test_exchange.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 882cf6b5a..3bfff50e8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -371,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert isclose(result, 2 * 1.1) + assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss))) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -383,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, 2 * 2 * 1.1) + assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss))) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -395,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(2, 2 * 2) * 1.1) + assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -407,10 +407,10 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(8, 2 * 2) * 1.1) + assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * 1.45) + assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(-0.4))) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) @@ -432,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), 8) def test_set_sandbox(default_conf, mocker): From ce23d9dfeef32c1a67f0cfbcc28fd39ff4305b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:38:08 +0100 Subject: [PATCH 0207/1386] Fix test min stake amount --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3bfff50e8..4531bf816 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -410,7 +410,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(-0.4))) + assert isclose(result, max(8, 2 * 2) * 1.5) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) From c9c039d640b59e0794b7c54ff30a1f8412e6c86d Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 15 Apr 2021 15:21:28 +0200 Subject: [PATCH 0208/1386] remove `copy()` from `custom_info` example `set_index` automatically copies if not stated otherwise with `inplace=True` > inplacebool, default False If True, modifies the DataFrame in place (do not create a new object). from: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.set_index.html?highlight=set_index#pandas.DataFrame.set_index --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7fa824a5b..96c927965 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -57,7 +57,7 @@ class AwesomeStrategy(IStrategy): dataframe['atr'] = ta.ATR(dataframe) if self.dp.runmode.value in ('backtest', 'hyperopt'): # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date') return dataframe ``` From 7142787256fd870193699929c377c0706a01599b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 15:41:35 +0200 Subject: [PATCH 0209/1386] Roll back unintended changes that break rendering --- docs/docker_quickstart.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index b133e33f0..9096000c1 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -14,7 +14,7 @@ To simplify running freqtrade, please install [`docker-compose`](https://docs.do ## Freqtrade with docker-compose -Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. +Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. !!! Note - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. @@ -24,8 +24,8 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory. -#### PC/MAC/Linux - +=== "PC/MAC/Linux" + ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -39,10 +39,10 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - - -#### RaspberryPi + ``` +=== "RaspberryPi" + ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -56,6 +56,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. @@ -67,11 +68,12 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. -### Question: "How to edit the bot configuration?" -You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. +!!! Question "How to edit the bot configuration?" + You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -##### Adding a custom strategy + You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. + +#### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` @@ -79,7 +81,7 @@ You can also change the both Strategy and commands by editing the command sectio The `SampleStrategy` is run by default. -#### Warning "`SampleStrategy` is just a demo!" +!!! Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest your strategy and use dry-run for some time before risking real money! You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). @@ -90,7 +92,7 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` -#### Warning "Default configuration" +!!! Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. #### Monitoring the bot @@ -120,7 +122,7 @@ docker-compose up -d This will first pull the latest image, and will then restart the container with the just pulled version. -#### Warning "Check the Changelog" +!!! Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. ### Editing the docker-compose file @@ -129,7 +131,7 @@ Advanced users may edit the docker-compose file further to include all possible All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. -#### Note "`docker-compose run --rm`" +!!! Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose From ce870bbcf7b7b4fe2dfd79884c2da33f225d4af3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 21:38:20 +0200 Subject: [PATCH 0210/1386] Use 3 decimals for ROI space --- freqtrade/optimize/hyperopt_interface.py | 10 +++++----- freqtrade/optimize/space/decimalspace.py | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 1bb471b9c..889854cad 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -140,7 +140,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_min'], 'roi_p3': roi_limits['roi_p3_min'], } - logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 5)}") + logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 3)}") p = { 'roi_t1': roi_limits['roi_t1_max'], 'roi_t2': roi_limits['roi_t2_max'], @@ -149,17 +149,17 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_max'], 'roi_p3': roi_limits['roi_p3_max'], } - logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 5)}") + logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") return [ Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), - SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=5, + SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=3, name='roi_p1'), - SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=5, + SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=3, name='roi_p2'), - SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=5, + SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=3, name='roi_p3'), ] diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index f5370b6d6..643999cc1 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -9,8 +9,9 @@ class SKDecimal(Integer): self.decimals = decimals _low = int(low * pow(10, self.decimals)) _high = int(high * pow(10, self.decimals)) - self.low_orig = low - self.high_orig = high + # trunc to precision to avoid points out of space + self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals) + self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals) super().__init__(_low, _high, prior, base, transform, name, dtype) From 5e51ba6258c77a33e7b06ed132159ade64029c7d Mon Sep 17 00:00:00 2001 From: grillzoo Date: Thu, 15 Apr 2021 21:38:00 +0100 Subject: [PATCH 0211/1386] fix flake8 --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea6bcb29f..ed7918b36 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -541,7 +541,7 @@ class Exchange: DEFAULT_AMOUNT_RESERVE_PERCENT) amount_reserve_percent = ( amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 - ) + ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4531bf816..27f4d0db9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -432,7 +432,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), 8) + assert round(result, 8) == round( + max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), + 8 + ) def test_set_sandbox(default_conf, mocker): From 01b303e0f95bf3c1dc4c7a375b82bcf2f29fc645 Mon Sep 17 00:00:00 2001 From: grillzoo Date: Thu, 15 Apr 2021 21:58:07 +0100 Subject: [PATCH 0212/1386] Aligning the doc --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index eb3351b8f..0ade558f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -167,7 +167,7 @@ This exchange has also a limit on USD - where all orders must be > 10$ - which h To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%). -With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`). +With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)`). If we take in account a stoploss of 10% on top of that - we'd end up with a value of ~14$ (`12.6 / (1 - 0.1)`). To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit. From 271e4500d984145a07de6c6de58f267a8b0cb904 Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Wed, 31 Mar 2021 18:08:53 -0300 Subject: [PATCH 0213/1386] telegram: Adding dynamic keyboard to /forcebuy response --- freqtrade/rpc/telegram.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 92899d67f..d263cb605 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -626,6 +626,10 @@ class Telegram(RPCHandler): self._rpc._rpc_forcebuy(pair, price) except RPCException as e: self._send_msg(str(e)) + else: + whitelist = self._rpc._rpc_whitelist()['whitelist'] + pairs_keyboard: List[List[Union[str, KeyboardButton]]] = [[f'/forcebuy {pair}' for pair in whitelist]] + self._send_msg("Which pair?", keyboard=pairs_keyboard) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -942,7 +946,8 @@ class Telegram(RPCHandler): ) def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False) -> None: + disable_notification: bool = False, + keyboard: List[List[Union[str, KeyboardButton]]] = None) -> None: """ Send given markdown message :param msg: message @@ -950,7 +955,9 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ - reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) + if keyboard is None: + keyboard = self._keyboard + reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( From e2f28991e6e5f3b7d429a3d50ad63ba6dd7f47c5 Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Wed, 31 Mar 2021 21:51:15 -0300 Subject: [PATCH 0214/1386] telegram: Wrapping long line --- freqtrade/rpc/telegram.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d263cb605..ae308a1cf 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -628,7 +628,9 @@ class Telegram(RPCHandler): self._send_msg(str(e)) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] - pairs_keyboard: List[List[Union[str, KeyboardButton]]] = [[f'/forcebuy {pair}' for pair in whitelist]] + pairs_keyboard: List[List[Union[str, KeyboardButton]]] = [ + [f'/forcebuy {pair}' for pair in whitelist] + ] self._send_msg("Which pair?", keyboard=pairs_keyboard) @authorized_only From 50bdae8eb200294136d03f0c287847aaf9b5386b Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Sat, 10 Apr 2021 20:13:04 -0300 Subject: [PATCH 0215/1386] telegram: Adding forcebuy inline keyboard --- freqtrade/rpc/telegram.py | 62 +++++++++++++++++++++++++++------- tests/rpc/test_rpc_telegram.py | 28 +++++++++++++++ 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ae308a1cf..b24638e48 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,9 +12,9 @@ from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update +from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update, BotCommand, InlineKeyboardMarkup, InlineKeyboardButton from telegram.error import NetworkError, TelegramError -from telegram.ext import CallbackContext, CommandHandler, Updater +from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ @@ -169,6 +169,11 @@ class Telegram(RPCHandler): [h.command for h in handles] ) + self._current_callback_query_handler = None + self._callback_query_handlers = { + 'forcebuy': CallbackQueryHandler(self._forcebuy_inline) + } + def cleanup(self) -> None: """ Stops all running telegram threads. @@ -610,6 +615,24 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) + def _forcebuy_action(self, pair, price = None): + try: + self._rpc._rpc_forcebuy(pair, price) + except RPCException as e: + self._send_msg(str(e)) + + def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None: + query = update.callback_query + pair = query.data + query.answer() + query.edit_message_text(text=f"Force Buying: {pair}") + self._forcebuy_action(pair) + + @staticmethod + def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], + cols=3) -> List[List[InlineKeyboardButton]]: + return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] + @authorized_only def _forcebuy(self, update: Update, context: CallbackContext) -> None: """ @@ -622,16 +645,13 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None - try: - self._rpc._rpc_forcebuy(pair, price) - except RPCException as e: - self._send_msg(str(e)) + self._forcebuy_action(pair, price) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] - pairs_keyboard: List[List[Union[str, KeyboardButton]]] = [ - [f'/forcebuy {pair}' for pair in whitelist] - ] - self._send_msg("Which pair?", keyboard=pairs_keyboard) + pairs = [InlineKeyboardButton(pair, callback_data=pair) for pair in whitelist] + self._send_inline_msg("Which pair?", + keyboard=self._layout_inline_keyboard(pairs), + callback_query_handler='forcebuy') @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -947,9 +967,27 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) + def _send_inline_msg(self, msg: str, callback_query_handler, + parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, + keyboard: List[List[InlineKeyboardButton]] = None, ) -> None: + """ + Send given markdown message + :param msg: message + :param bot: alternative bot + :param parse_mode: telegram parse mode + :return: None + """ + if self._current_callback_query_handler: + self._updater.dispatcher.remove_handler(self._current_callback_query_handler) + self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler] + self._updater.dispatcher.add_handler(self._current_callback_query_handler) + + self._send_msg(msg, parse_mode, disable_notification, keyboard, reply_markup=InlineKeyboardMarkup) + def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[Union[str, KeyboardButton]]] = None) -> None: + keyboard: List[List[Union[str, KeyboardButton]]] = None, + reply_markup=ReplyKeyboardMarkup) -> None: """ Send given markdown message :param msg: message @@ -959,7 +997,7 @@ class Telegram(RPCHandler): """ if keyboard is None: keyboard = self._keyboard - reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) + reply_markup = reply_markup(keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 27babb1b7..7fdbf77f7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -6,6 +6,7 @@ import re from datetime import datetime from random import choice, randint from string import ascii_uppercase +from functools import reduce from unittest.mock import ANY, MagicMock import arrow @@ -53,6 +54,12 @@ class DummyCls(Telegram): """ raise Exception('test') +def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=None): + inline_msg_mock = MagicMock() + telegram, ftbot, msg_mock = get_telegram_testobject(mocker, default_conf) + mocker.patch('freqtrade.rpc.telegram.Telegram._send_inline_msg', inline_msg_mock) + + return telegram, ftbot, msg_mock, inline_msg_mock def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): msg_mock = MagicMock() @@ -901,6 +908,27 @@ def test_forcebuy_handle_exception(default_conf, update, mocker) -> None: assert msg_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' +def test_forcebuy_no_pair(default_conf, update, mocker) -> None: + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + + fbuy_mock = MagicMock(return_value=None) + mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) + + telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + + context = MagicMock() + context.args = [] + telegram._forcebuy(update=update, context=context) + + assert fbuy_mock.call_count == 0 + assert inline_msg_mock.call_count == 1 + assert inline_msg_mock.call_args_list[0][0][0] == 'Which pair?' + assert inline_msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' + keyboard = inline_msg_mock.call_args_list[0][1]['keyboard'] + assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 + + def test_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None: From 5fae4ea2fdcc46f31117b7d64dd1b477e7b8ffe3 Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Wed, 14 Apr 2021 17:52:47 -0300 Subject: [PATCH 0216/1386] telegram: Formatting code --- freqtrade/rpc/telegram.py | 12 ++++++++---- tests/rpc/test_rpc_telegram.py | 5 ++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b24638e48..52cd7a5ed 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,7 +12,10 @@ from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update, BotCommand, InlineKeyboardMarkup, InlineKeyboardButton +from telegram import ( + KeyboardButton, ParseMode, ReplyKeyboardMarkup, + Update, InlineKeyboardMarkup, InlineKeyboardButton + ) from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler from telegram.utils.helpers import escape_markdown @@ -615,7 +618,7 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) - def _forcebuy_action(self, pair, price = None): + def _forcebuy_action(self, pair, price=None): try: self._rpc._rpc_forcebuy(pair, price) except RPCException as e: @@ -969,7 +972,7 @@ class Telegram(RPCHandler): def _send_inline_msg(self, msg: str, callback_query_handler, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[InlineKeyboardButton]] = None, ) -> None: + keyboard: List[List[InlineKeyboardButton]] = None, ) -> None: """ Send given markdown message :param msg: message @@ -982,7 +985,8 @@ class Telegram(RPCHandler): self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler] self._updater.dispatcher.add_handler(self._current_callback_query_handler) - self._send_msg(msg, parse_mode, disable_notification, keyboard, reply_markup=InlineKeyboardMarkup) + self._send_msg(msg, parse_mode, disable_notification, keyboard, + reply_markup=InlineKeyboardMarkup) def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7fdbf77f7..c80709ac1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -54,6 +54,7 @@ class DummyCls(Telegram): """ raise Exception('test') + def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=None): inline_msg_mock = MagicMock() telegram, ftbot, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -61,6 +62,7 @@ def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=N return telegram, ftbot, msg_mock, inline_msg_mock + def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): msg_mock = MagicMock() if mock: @@ -914,7 +916,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: fbuy_mock = MagicMock(return_value=None) mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) - telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker, default_conf) + telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker, + default_conf) patch_get_signal(freqtradebot, (True, False)) context = MagicMock() From e3c5a4b3fc8b1e33eca8c3313d42889e20bf8b0b Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Wed, 14 Apr 2021 18:20:10 -0300 Subject: [PATCH 0217/1386] telegram: Formatting imports --- freqtrade/rpc/telegram.py | 8 +++----- tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 52cd7a5ed..8a25fc61f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,12 +12,10 @@ from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import ( - KeyboardButton, ParseMode, ReplyKeyboardMarkup, - Update, InlineKeyboardMarkup, InlineKeyboardButton - ) +from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode, + ReplyKeyboardMarkup, Update) from telegram.error import NetworkError, TelegramError -from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler +from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index c80709ac1..a3147f956 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -4,9 +4,9 @@ import re from datetime import datetime +from functools import reduce from random import choice, randint from string import ascii_uppercase -from functools import reduce from unittest.mock import ANY, MagicMock import arrow From 7a98de10ea6cea4c13e74fc3c1cc9ae36817cf6c Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Thu, 15 Apr 2021 09:31:20 -0300 Subject: [PATCH 0218/1386] telegram: Formatting typings --- freqtrade/rpc/telegram.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8a25fc61f..0d978cc6e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -8,7 +8,7 @@ import logging from datetime import timedelta from html import escape from itertools import chain -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List, Optional, Union, cast import arrow from tabulate import tabulate @@ -88,7 +88,7 @@ class Telegram(RPCHandler): Validates the keyboard configuration from telegram config section. """ - self._keyboard: List[List[Union[str, KeyboardButton]]] = [ + self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [ ['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help'] @@ -170,7 +170,7 @@ class Telegram(RPCHandler): [h.command for h in handles] ) - self._current_callback_query_handler = None + self._current_callback_query_handler: Optional[CallbackQueryHandler] = None self._callback_query_handlers = { 'forcebuy': CallbackQueryHandler(self._forcebuy_inline) } @@ -623,11 +623,12 @@ class Telegram(RPCHandler): self._send_msg(str(e)) def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None: - query = update.callback_query - pair = query.data - query.answer() - query.edit_message_text(text=f"Force Buying: {pair}") - self._forcebuy_action(pair) + if update.callback_query: + query = update.callback_query + pair = query.data + query.answer() + query.edit_message_text(text=f"Force Buying: {pair}") + self._forcebuy_action(pair) @staticmethod def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], @@ -983,12 +984,13 @@ class Telegram(RPCHandler): self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler] self._updater.dispatcher.add_handler(self._current_callback_query_handler) - self._send_msg(msg, parse_mode, disable_notification, keyboard, + self._send_msg(msg, parse_mode, disable_notification, + cast(List[List[Union[str, KeyboardButton, InlineKeyboardButton]]], keyboard), reply_markup=InlineKeyboardMarkup) def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[Union[str, KeyboardButton]]] = None, + keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None, reply_markup=ReplyKeyboardMarkup) -> None: """ Send given markdown message From 2011912a19620c337b37a777123e80f86f0c9ebb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 07:46:00 +0200 Subject: [PATCH 0219/1386] Adapt documentation to use 3 decimals only --- docs/hyperopt.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 07cc963cf..0e6bded92 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -460,23 +460,26 @@ As stated in the comment, you can also use it as the value of the `minimal_roi` #### Default ROI Search Space -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 3 digits after the decimal point): -| # step | 1m | | 5m | | 1h | | 1d | | -| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | -| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | -| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | -| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | -| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | +| # step | 1m | | 5m | | 1h | | 1d | | +| ------ | ------ | ------------- | -------- | ----------- | ---------- | ------------- | ------------ | ------------- | +| 1 | 0 | 0.011...0.119 | 0 | 0.03...0.31 | 0 | 0.068...0.711 | 0 | 0.121...1.258 | +| 2 | 2...8 | 0.007...0.042 | 10...40 | 0.02...0.11 | 120...480 | 0.045...0.252 | 2880...11520 | 0.081...0.446 | +| 3 | 4...20 | 0.003...0.015 | 20...100 | 0.01...0.04 | 240...1200 | 0.022...0.091 | 5760...28800 | 0.040...0.162 | +| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Understand Hyperopt Stoploss results If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: @@ -516,6 +519,9 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Understand Hyperopt Trailing Stop results If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: @@ -551,6 +557,9 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Reproducible results The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. From 8ce5522a100f2058f7ba20a662fbfba3eb78cf71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 08:00:04 +0200 Subject: [PATCH 0220/1386] Add additional documentation for SKDecimal space --- docs/advanced-hyperopt.md | 24 +++++++++++++++++++++++- docs/hyperopt.md | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index cc71f39a7..c86978b80 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -79,9 +79,31 @@ class MyAwesomeStrategy(IStrategy): class HyperOpt: # Define a custom stoploss space. def stoploss_space(self): - return [Real(-0.05, -0.01, name='stoploss')] + return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')] ``` +## Space options + +For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: + +* `Categorical` - Pick from a list of categories (e.g. `Categorical(['a', 'b', 'c'], name="cat")`) +* `Integer` - Pick from a range of whole numbers (e.g. `Integer(1, 10, name='rsi')`) +* `SKDecimal` - Pick from a range of decimal numbers with limited precision (e.g. `SKDecimal(0.1, 0.5, decimals=3, name='adx')`). *Available only with freqtrade*. +* `Real` - Pick from a range of decimal numbers with full precision (e.g. `Real(0.1, 0.5, name='adx')` + +You can import all of these from `freqtrade.optimize.space`, although `Categorical`, `Integer` and `Real` are only aliases for their corresponding scikit-optimize Spaces. `SKDecimal` is provided by freqtrade for faster optimizations. + +``` python +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa +``` + +!!! Hint "SKDecimal vs. Real" + We recommend to use `SKDecimal` instead of the `Real` space in almost all cases. While the Real space provides full accuracy (up to ~16 decimal places) - this precision is rarely needed, and leads to unnecessary long hyperopt times. + + Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`). + + A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`). + --- ## Legacy Hyperopt diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0e6bded92..b073a73b6 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -294,6 +294,7 @@ Based on the results, hyperopt will tell you which parameter combination produce ## Parameter types There are four parameter types each suited for different purposes. + * `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. * `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. * `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities. From e6936ae1352246125a66685abf9ac35d05fbce3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:16:29 +0200 Subject: [PATCH 0221/1386] Improve wording in docs --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b073a73b6..21cbadf7f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -479,7 +479,7 @@ Override the `roi_space()` method if you need components of the ROI tables to va A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Stoploss results @@ -521,7 +521,7 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Trailing Stop results @@ -559,7 +559,7 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Reproducible results From aeb81f90ff0cf375def82f21194c922baa4c4b76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:35:56 +0200 Subject: [PATCH 0222/1386] Implement errorhandling for /trade endpoint --- freqtrade/rpc/api_server/api_v1.py | 5 ++++- tests/rpc/test_rpc_apiserver.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 6873c0c4c..02736aca6 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -90,7 +90,10 @@ def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_status([tradeid])[0] + try: + return rpc._rpc_trade_status([tradeid])[0] + except (RPCException, KeyError): + raise HTTPException(status_code=404, detail='Trade not found.') @router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a65b4ed6f..760d78b03 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -522,6 +522,26 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 1 +def test_api_trade_single(botclient, mocker, fee, ticker, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + fetch_ticker=ticker, + ) + rc = client_get(client, f"{BASE_URI}/trade/3") + assert_response(rc, 404) + assert rc.json()['detail'] == 'Trade not found.' + + create_mock_trades(fee) + Trade.query.session.flush() + + rc = client_get(client, f"{BASE_URI}/trade/3") + assert_response(rc) + assert rc.json()['trade_id'] == 3 + + def test_api_delete_trade(botclient, mocker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From 5c579613e1cf8505f659210ecae518168a0f026b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:42:13 +0200 Subject: [PATCH 0223/1386] add /trade endpoint to rest_client script --- docs/rest-api.md | 12 ++++++++---- scripts/rest_client.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index be3107fcb..5c25e9eeb 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -235,6 +235,9 @@ pair_history performance Return the performance of the different coins. +ping + simple ping + plot_config Return plot configuration if the strategy defines one. @@ -271,15 +274,16 @@ strategy :param strategy: Strategy class name +trade + Return specific trade + + :param trade_id: Specify which trade to get. + trades Return trades history. :param limit: Limits trades to the X last trades. No limit to get all the trades. -trade - Return specific trade. - :param tradeid: Specify which trade to get. - version Return the version of the bot. diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4d667879d..40b338ce8 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -127,7 +127,7 @@ class FtRestClient(): return self._delete("locks/{}".format(lock_id)) def daily(self, days=None): - """Return the amount of open trades. + """Return the profits for each day, and amount of trades. :return: json object """ @@ -195,7 +195,7 @@ class FtRestClient(): def logs(self, limit=None): """Show latest logs. - :param limit: Limits log messages to the last logs. No limit to get all the trades. + :param limit: Limits log messages to the last logs. No limit to get the entire log. :return: json object """ return self._get("logs", params={"limit": limit} if limit else 0) @@ -208,6 +208,14 @@ class FtRestClient(): """ return self._get("trades", params={"limit": limit} if limit else 0) + def trade(self, trade_id): + """Return specific trade + + :param trade_id: Specify which trade to get. + :return: json object + """ + return self._get("trade/{}".format(trade_id)) + def delete_trade(self, trade_id): """Delete trade from the database. Tries to close open orders. Requires manual handling of this asset on the exchange. From 1eb9ce4227a0424b5153e8f47d1522fe2ac1de29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:47:32 +0200 Subject: [PATCH 0224/1386] Allow specifying pairs for optimize commands via `--pairs` --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/configuration/configuration.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9468a7f7d..9cf9992ce 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -17,7 +17,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", - "max_open_trades", "stake_amount", "fee"] + "max_open_trades", "stake_amount", "fee", "pairs"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "enable_protections", "dry_run_wallet", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 4fac8ac72..e49895de4 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -330,7 +330,7 @@ AVAILABLE_CLI_OPTIONS = { # Script options "pairs": Arg( '-p', '--pairs', - help='Show profits for only these pairs. Pairs are space-separated.', + help='Limit command to these pairs. Pairs are space-separated.', nargs='+', ), # Download data diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 9acd532cc..cc11f97c2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -445,6 +445,7 @@ class Configuration: """ if "pairs" in config: + config['exchange']['pair_whitelist'] = config['pairs'] return if "pairs_file" in self.args and self.args["pairs_file"]: From 6a9c47d15f7476f72dfb220e643a8801bcf5d058 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:48:24 +0200 Subject: [PATCH 0225/1386] Update docs with new options --- docs/backtesting.md | 6 +++++- docs/edge.md | 10 +++++++++- docs/hyperopt.md | 11 ++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index e16225f94..ee9926f32 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -15,7 +15,8 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--eps] [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -37,6 +38,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). diff --git a/docs/edge.md b/docs/edge.md index 7f0a9cb2d..237ff36f6 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -215,8 +215,10 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TIMEFRAME] [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] - [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] + [--fee FLOAT] [-p PAIRS [PAIRS ...]] + [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit @@ -224,6 +226,9 @@ optional arguments: Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). --max-open-trades INT Override the value of the `max_open_trades` configuration setting. @@ -232,6 +237,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --stoplosses STOPLOSS_RANGE Defines a range of stoploss values against which edge will assess the strategy. The format is "min,max,step" diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 21cbadf7f..51905e616 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -44,8 +44,9 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--hyperopt NAME] + [--hyperopt-path PATH] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] @@ -69,6 +70,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --hyperopt NAME Specify hyperopt class name which will be used by the bot. --hyperopt-path PATH Specify additional lookup path for Hyperopt and @@ -105,7 +109,8 @@ optional arguments: reproducible hyperopt results. --min-trades INT Set minimal desired number of trades for evaluations in the hyperopt optimization path (default: 1). - --hyperopt-loss NAME Specify the class name of the hyperopt loss function + --hyperopt-loss NAME, --hyperoptloss NAME + Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in From c8d3d449a3b0fee283999d5782939dc0bbc7ff5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:51:02 +0200 Subject: [PATCH 0226/1386] Add quick test for pair_whitelist overwrite --- tests/test_configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index a512bf58a..b2c883108 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1002,6 +1002,7 @@ def test_pairlist_resolving(): config = configuration.get_config() assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['pair_whitelist'] == ['ETH/BTC', 'XRP/BTC'] assert config['exchange']['name'] == 'binance' From fbb90755395e205f2235229dc2eb14c25ee88c99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:53:03 +0200 Subject: [PATCH 0227/1386] Update util command structures too --- docs/data-download.md | 6 +++--- docs/plotting.md | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 04f444a8b..7a78334d5 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -30,7 +30,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. @@ -48,10 +48,10 @@ optional arguments: exchange/pairs/timeframes. --data-format-ohlcv {json,jsongz,hdf5} Storage format for downloaded candle (OHLCV) data. - (default: `json`). + (default: `None`). --data-format-trades {json,jsongz,hdf5} Storage format for downloaded trades data. (default: - `jsongz`). + `None`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/plotting.md b/docs/plotting.md index 63afa16b6..5d454c414 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -37,7 +37,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --indicators1 INDICATORS1 [INDICATORS1 ...] Set indicators from your strategy you want in the @@ -90,6 +90,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` Example: @@ -244,7 +245,7 @@ usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --timerange TIMERANGE Specify what timerange of data to use. @@ -286,6 +287,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` The `-p/--pairs` argument, can be used to limit the pairs that are considered for this calculation. From 44bfb53668d8a5fd301e0c2187623a6e958589a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 19:29:34 +0200 Subject: [PATCH 0228/1386] Don't use current rate for closed trades --- freqtrade/rpc/api_server/api_schemas.py | 1 - freqtrade/rpc/rpc.py | 11 +++++++---- tests/rpc/test_rpc_apiserver.py | 1 - 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 41de0134c..12bee1cf2 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -189,7 +189,6 @@ class OpenTradeSchema(TradeSchema): stoploss_current_dist_ratio: Optional[float] stoploss_entry_dist: Optional[float] stoploss_entry_dist_ratio: Optional[float] - base_currency: str current_profit: float current_profit_abs: float current_profit_pct: float diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 88f8b36db..b86562e80 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -167,10 +167,13 @@ class RPC: if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user - try: - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except (ExchangeError, PricingError): - current_rate = NAN + if trade.is_open: + try: + current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + except (ExchangeError, PricingError): + current_rate = NAN + else: + current_rate = trade.close_rate current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3d27922d8..d610906d5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -773,7 +773,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert rc.json()[0] == { 'amount': 123.0, 'amount_requested': 123.0, - 'base_currency': 'BTC', 'close_date': None, 'close_timestamp': None, 'close_profit': None, From 0737e3fa2230ba27dee588476fa589691123554e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 19:48:29 +0200 Subject: [PATCH 0229/1386] Clarify refresh_period section for volumepairlist part of #4689 --- docs/includes/pairlists.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8688494cc..85d157e75 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -60,6 +60,8 @@ When used in the chain of Pairlist Handlers in a non-leading position (after Sta When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). +The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. +Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data. `VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library: @@ -90,6 +92,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at #### PerformanceFilter Sorts pairs by past trade performance, as follows: + 1. Positive performance. 2. No closed trades yet. 3. Negative performance. From 296ea30cc341345bd87a46f99bbe99e511481ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:35 +0000 Subject: [PATCH 0230/1386] Bump pytest-asyncio from 0.14.0 to 0.15.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.14.0 to 0.15.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.14.0...v0.15.0) 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 cd93f2433..6ddbffb74 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 pytest==6.2.3 -pytest-asyncio==0.14.0 +pytest-asyncio==0.15.0 pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-random-order==1.0.4 From 8d2e6954a1fabb833b6e75a5609a4993aeac90bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:41 +0000 Subject: [PATCH 0231/1386] Bump flake8 from 3.9.0 to 3.9.1 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.0 to 3.9.1. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.0...3.9.1) 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 cd93f2433..35c57769a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==3.0.1 -flake8==3.9.0 +flake8==3.9.1 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 From 05246e6637637590909e9e34d0a3a813cc14aa5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:52 +0000 Subject: [PATCH 0232/1386] Bump pandas from 1.2.3 to 1.2.4 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.2.3...v1.2.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 129bb05b2..c6051463c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.20.2 -pandas==1.2.3 +pandas==1.2.4 ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels From 59d02f3f039f4e696ac78f7b2eeb3d7888020224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:23:27 +0000 Subject: [PATCH 0233/1386] Bump sqlalchemy from 1.4.7 to 1.4.9 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.7 to 1.4.9. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 129bb05b2..5159d69e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.7 +SQLAlchemy==1.4.9 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From b94de3030a11610d48aae70f9e9f182e098c0096 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:23:33 +0000 Subject: [PATCH 0234/1386] Bump mkdocs-material from 7.1.1 to 7.1.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.1...7.1.2) 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 cfd63d1d0..4d7082a7f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.1 +mkdocs-material==7.1.2 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 9407dbcf87eea8a511642045b2910f54de448f49 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 07:46:39 +0200 Subject: [PATCH 0235/1386] Add freqtrade powered by ccxt --- README.md | 2 +- docs/assets/ccxt-logo.svg | 3 ++ docs/assets/freqtrade_poweredby.svg | 44 +++++++++++++++++++++++++++++ docs/index.md | 5 ++-- 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 docs/assets/ccxt-logo.svg create mode 100644 docs/assets/freqtrade_poweredby.svg diff --git a/README.md b/README.md index c3a665c47..916f9cf17 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Freqtrade +# ![freqtrade](docs/assets/freqtrade_poweredby.svg) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) diff --git a/docs/assets/ccxt-logo.svg b/docs/assets/ccxt-logo.svg new file mode 100644 index 000000000..e52682546 --- /dev/null +++ b/docs/assets/ccxt-logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg new file mode 100644 index 000000000..1041f87ab --- /dev/null +++ b/docs/assets/freqtrade_poweredby.svg @@ -0,0 +1,44 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + Freqtrade + + + + + poweredby + + diff --git a/docs/index.md b/docs/index.md index 61f2276c3..c2b6d5629 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ -# Freqtrade +![freqtrade](assets/freqtrade_poweredby.svg) + [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) @@ -39,7 +40,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Bittrex](https://bittrex.com/) - [X] [FTX](https://ftx.com) - [X] [Kraken](https://kraken.com/) -- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +- [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested From 66b3ecfeed6023794d9710dde825f49d46ed0bd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 08:32:17 +0200 Subject: [PATCH 0236/1386] Remove faulty font-family in svg --- docs/assets/freqtrade_poweredby.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg index 1041f87ab..71d165cbf 100644 --- a/docs/assets/freqtrade_poweredby.svg +++ b/docs/assets/freqtrade_poweredby.svg @@ -34,7 +34,7 @@ - Freqtrade + Freqtrade From 0ddc68b37d82bb7ee64ce0851cd050bce0b3539f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 09:41:49 +0000 Subject: [PATCH 0237/1386] Bump ccxt from 1.47.47 to 1.48.22 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.47.47 to 1.48.22. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.47.47...1.48.22) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d71cd1c05..a89eb2383 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.4 -ccxt==1.47.47 +ccxt==1.48.22 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From a2acb54e7efc90fa2ce8af85284518cb840d26fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 15:15:40 +0200 Subject: [PATCH 0238/1386] Clarify comments in pairlist --- freqtrade/plugins/pairlist/IPairList.py | 2 +- freqtrade/plugins/pairlist/StaticPairList.py | 2 +- freqtrade/plugins/pairlist/VolumePairList.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 184feff9e..c4a9c3e40 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -85,7 +85,7 @@ class IPairList(LoggingMixin, ABC): position in the chain. :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ raise OperationalException("This Pairlist Handler should not be used " diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index c5ced48c9..13d30fc47 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -46,7 +46,7 @@ class StaticPairList(IPairList): """ Generate the pairlist :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ if self._allow_inactive: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index dd8fc64fd..e85fb1805 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -67,7 +67,7 @@ class VolumePairList(IPairList): """ Generate the pairlist :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ # Generate dynamic whitelist From 75612496d7294ca6b0ccd89a62cb16562109d307 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:01:39 +0200 Subject: [PATCH 0239/1386] Improve poweredBy logo spacing --- docs/assets/freqtrade_poweredby.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg index 71d165cbf..957ec6401 100644 --- a/docs/assets/freqtrade_poweredby.svg +++ b/docs/assets/freqtrade_poweredby.svg @@ -34,11 +34,11 @@ - Freqtrade + Freqtrade - poweredby + poweredby From c9e901cf325756707e51e4dbda2f060b2fed827b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 11:31:37 +0200 Subject: [PATCH 0240/1386] Move wallet tasks to test_wallets --- tests/test_freqtradebot.py | 59 -------------------------------------- tests/test_wallets.py | 59 +++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 60 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c91015766..0634df9e4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -207,65 +207,6 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b freqtrade.get_free_open_trades()) -def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) - - -@pytest.mark.parametrize("balance_ratio,result1", [ - (1, 0.005), - (0.99, 0.00495), - (0.50, 0.0025), - ]) -def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - limit_buy_order_open, fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), - get_fee=fee - ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - conf['dry_run_wallet'] = 0.01 - conf['max_open_trades'] = 2 - conf['tradable_balance_ratio'] = balance_ratio - - freqtrade = FreqtradeBot(conf) - patch_get_signal(freqtrade) - - # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) - assert result == result1 - - # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.execute_buy('ETH/BTC', result) - - result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) - assert result == result1 - - # create 2 trades, order amount should be None - freqtrade.execute_buy('LTC/BTC', result) - - result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) - assert result == 0 - - # set max_open_trades = None, so do not trade - conf['max_open_trades'] = 0 - freqtrade = FreqtradeBot(conf) - result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) - assert result == 0 - - def test_edge_called_in_process(mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_edge(mocker) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index b7aead0c4..e6e41bab1 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1,7 +1,12 @@ # pragma pylint: disable=missing-docstring +from copy import deepcopy from unittest.mock import MagicMock -from tests.conftest import get_patched_freqtradebot +import pytest + +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import DependencyException +from tests.conftest import get_patched_freqtradebot, patch_wallet def test_sync_wallet_at_boot(mocker, default_conf): @@ -106,3 +111,55 @@ def test_sync_wallet_missing_data(mocker, default_conf): assert freqtrade.wallets._wallets['GAS'].used is None assert freqtrade.wallets._wallets['GAS'].total == 0.260739 assert freqtrade.wallets.get_free('GAS') == 0.260739 + + +def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: + patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + with pytest.raises(DependencyException, match=r'.*stake amount.*'): + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + + +@pytest.mark.parametrize("balance_ratio,result1", [ + (1, 0.005), + (0.99, 0.00495), + (0.50, 0.0025), +]) +def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, + limit_buy_order_open, fee, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + buy=MagicMock(return_value=limit_buy_order_open), + get_fee=fee + ) + + conf = deepcopy(default_conf) + conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT + conf['dry_run_wallet'] = 0.01 + conf['max_open_trades'] = 2 + conf['tradable_balance_ratio'] = balance_ratio + + freqtrade = get_patched_freqtradebot(mocker, conf) + + # no open trades, order amount should be 'balance / max_open_trades' + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + assert result == result1 + + # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' + freqtrade.execute_buy('ETH/BTC', result) + + result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) + assert result == result1 + + # create 2 trades, order amount should be None + freqtrade.execute_buy('LTC/BTC', result) + + result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) + assert result == 0 + + # set max_open_trades = None, so do not trade + freqtrade.config['max_open_trades'] = 0 + result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) + assert result == 0 From bd7e535e42ccca777697f497d0e9264698c4421a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 11:58:47 +0200 Subject: [PATCH 0241/1386] Use "human" amounts in stake_amount tests --- tests/test_wallets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index e6e41bab1..562957790 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -122,9 +122,9 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: @pytest.mark.parametrize("balance_ratio,result1", [ - (1, 0.005), - (0.99, 0.00495), - (0.50, 0.0025), + (1, 50), + (0.99, 49.5), + (0.50, 25), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, limit_buy_order_open, fee, mocker) -> None: @@ -137,29 +137,29 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r conf = deepcopy(default_conf) conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - conf['dry_run_wallet'] = 0.01 + conf['dry_run_wallet'] = 100 conf['max_open_trades'] = 2 conf['tradable_balance_ratio'] = balance_ratio freqtrade = get_patched_freqtradebot(mocker, conf) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades()) assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.execute_buy('ETH/BTC', result) + freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) assert result == 0 # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 - result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) assert result == 0 From 2254f65fa7f8c32d2d23d559fa57f297bf424dd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 12:54:22 +0200 Subject: [PATCH 0242/1386] use binance intests instead of bittrex --- tests/commands/test_commands.py | 4 +- tests/config_test_comments.json | 2 +- tests/conftest.py | 8 ++-- tests/conftest_trades.py | 12 ++--- tests/plugins/test_protections.py | 2 +- tests/rpc/test_rpc.py | 4 +- tests/rpc/test_rpc_apiserver.py | 8 ++-- tests/rpc/test_rpc_manager.py | 2 +- tests/rpc/test_rpc_telegram.py | 22 ++++----- tests/rpc/test_rpc_webhook.py | 10 ++-- tests/strategy/test_interface.py | 8 ++-- tests/test_freqtradebot.py | 77 ++++++++++++++++++++----------- tests/test_persistence.py | 54 +++++++++++----------- 13 files changed, 117 insertions(+), 96 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 232fc4e2c..d86bced5d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -116,7 +116,7 @@ def test_list_timeframes(mocker, capsys): '1h': 'hour', '1d': 'day', } - patch_exchange(mocker, api_mock=api_mock) + patch_exchange(mocker, api_mock=api_mock, id='bittrex') args = [ "list-timeframes", ] @@ -201,7 +201,7 @@ def test_list_markets(mocker, markets, capsys): api_mock = MagicMock() api_mock.markets = markets - patch_exchange(mocker, api_mock=api_mock) + patch_exchange(mocker, api_mock=api_mock, id='bittrex') # Test with no --config args = [ diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 4f201f86c..48a087dec 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -59,7 +59,7 @@ } }, "exchange": { - "name": "bittrex", + "name": "binance", "sandbox": false, "key": "your_exchange_key", "secret": "your_exchange_secret", diff --git a/tests/conftest.py b/tests/conftest.py index 4a2106a4d..cc4fe91f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,7 +79,7 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: +def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -98,7 +98,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', +def get_patched_exchange(mocker, config, api_mock=None, id='binance', mock_markets=True) -> Exchange: patch_exchange(mocker, api_mock, id, mock_markets) config['exchange']['name'] = id @@ -293,7 +293,7 @@ def get_default_conf(testdatadir): "order_book_max": 1 }, "exchange": { - "name": "bittrex", + "name": "binance", "enabled": True, "key": "key", "secret": "secret", @@ -1765,7 +1765,7 @@ def open_trade(): return Trade( pair='ETH/BTC', open_rate=0.00001099, - exchange='bittrex', + exchange='binance', open_order_id='123456789', amount=90.99181073, fee_open=0.0, diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 34fc58aee..b92b51144 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -31,7 +31,7 @@ def mock_trade_1(fee): is_open=True, open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', timeframe=5, @@ -84,7 +84,7 @@ def mock_trade_2(fee): close_rate=0.128, close_profit=0.005, close_profit_abs=0.000584127, - exchange='bittrex', + exchange='binance', is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', @@ -144,7 +144,7 @@ def mock_trade_3(fee): close_rate=0.06, close_profit=0.01, close_profit_abs=0.000155, - exchange='bittrex', + exchange='binance', is_open=False, strategy='DefaultStrategy', timeframe=5, @@ -187,7 +187,7 @@ def mock_trade_4(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14), is_open=True, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='prod_buy_12345', strategy='DefaultStrategy', timeframe=5, @@ -239,7 +239,7 @@ def mock_trade_5(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), is_open=True, open_rate=0.123, - exchange='bittrex', + exchange='binance', strategy='SampleStrategy', stoploss_order_id='prod_stoploss_3455', timeframe=5, @@ -293,7 +293,7 @@ def mock_trade_6(fee): fee_close=fee.return_value, is_open=True, open_rate=0.15, - exchange='bittrex', + exchange='binance', strategy='SampleStrategy', open_order_id="prod_sell_6", timeframe=5, diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 545387eaa..a39301145 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -27,7 +27,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, open_rate=open_rate, is_open=is_open, amount=0.01 / open_rate, - exchange='bittrex', + exchange='binance', ) trade.recalc_open_trade_value() if not is_open: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 63e09cbe1..6d31e7635 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -106,7 +106,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', @@ -172,7 +172,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d610906d5..6505629eb 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -468,7 +468,7 @@ def test_api_show_config(botclient, mocker): rc = client_get(client, f"{BASE_URI}/show_config") assert_response(rc) assert 'dry_run' in rc.json() - assert rc.json()['exchange'] == 'bittrex' + assert rc.json()['exchange'] == 'binance' assert rc.json()['timeframe'] == '5m' assert rc.json()['timeframe_ms'] == 300000 assert rc.json()['timeframe_min'] == 5 @@ -825,7 +825,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, - 'exchange': 'bittrex', + 'exchange': 'binance', } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', @@ -916,7 +916,7 @@ def test_api_forcebuy(botclient, mocker, fee): pair='ETH/ETH', amount=1, amount_requested=1, - exchange='bittrex', + exchange='binance', stake_amount=1, open_rate=0.245441, open_order_id="123456", @@ -979,7 +979,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, - 'exchange': 'bittrex', + 'exchange': 'binance', } diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 3068e9764..6996c932b 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -140,7 +140,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: rpc_manager.startup_messages(default_conf, freqtradebot.pairlists, freqtradebot.protections) assert telegram_mock.call_count == 3 - assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status'] + assert "*Exchange:* `binance`" in telegram_mock.call_args_list[1][0][0]['status'] telegram_mock.reset_mock() default_conf['dry_run'] = True diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 34bf057cb..ba32dc385 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -688,7 +688,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.173e-05, @@ -749,7 +749,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.043e-05, @@ -800,7 +800,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.099e-05, @@ -1178,7 +1178,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] - assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] @@ -1187,7 +1187,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] - assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] @@ -1197,7 +1197,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', @@ -1213,7 +1213,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ - == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \ + == '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1242,11 +1242,11 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'reason': CANCEL_REASON['TIMEOUT'] }) - assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' + assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* ' 'Cancelling open buy Order for ETH/BTC (#1). ' 'Reason: cancelled due to timeout.') @@ -1393,7 +1393,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', @@ -1405,7 +1405,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' + assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 5361cd947..62818ecbb 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -59,7 +59,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'stake_amount': 0.8, @@ -80,7 +80,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'stake_amount': 0.8, @@ -101,7 +101,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, @@ -127,7 +127,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, @@ -184,7 +184,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'order_type': 'limit', diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 0ee80e0c5..78fa368e4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -219,7 +219,7 @@ def test_min_roi_reached(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -258,7 +258,7 @@ def test_min_roi_reached2(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -293,7 +293,7 @@ def test_min_roi_reached3(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -346,7 +346,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) trade.adjust_min_max_rates(trade.open_rate) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0634df9e4..433cce170 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -362,7 +362,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -621,7 +621,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.stake_amount == default_conf['stake_amount'] assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' assert trade.open_rate == 0.00001098 assert trade.amount == 91.07468123 @@ -718,7 +718,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, is_open=True, amount=20, open_rate=0.01, - exchange='bittrex', + exchange='binance', )) Trade.query.session.add(Trade( pair='ETH/BTC', @@ -728,7 +728,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, is_open=True, amount=12, open_rate=0.001, - exchange='bittrex', + exchange='binance', )) assert pair not in freqtrade.active_pair_whitelist @@ -969,7 +969,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None return_value=limit_buy_order['amount']) stoploss = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1001,6 +1001,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss ) freqtrade = FreqtradeBot(default_conf) @@ -1025,7 +1028,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1038,7 +1041,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1064,14 +1067,14 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert trade.stoploss_order_id is None assert trade.is_open is False mocker.patch( - 'freqtrade.exchange.Exchange.stoploss', + 'freqtrade.exchange.Binance.stoploss', side_effect=ExchangeError() ) trade.is_open = True @@ -1083,9 +1086,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) assert stoploss.call_count == 1 @@ -1095,7 +1098,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.is_open = False stoploss.reset_mock() mocker.patch('freqtrade.exchange.Exchange.fetch_order') - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1115,6 +1118,9 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), stoploss=MagicMock(side_effect=ExchangeError()), ) @@ -1149,6 +1155,9 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value=limit_buy_order_open), sell=sell_mock, get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', fetch_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=InvalidOrderException()), ) @@ -1194,6 +1203,9 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, sell=sell_mock, get_fee=fee, fetch_order=MagicMock(return_value={'status': 'canceled'}), + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=MagicMock(side_effect=InsufficientFundsError()), ) patch_get_signal(freqtrade) @@ -1231,6 +1243,9 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1271,7 +1286,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, } }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1286,8 +1301,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1334,6 +1349,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1369,9 +1387,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 'stopPrice': '0.1' } } - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1380,8 +1398,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() - cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError()) + cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) + mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1403,6 +1421,9 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1443,7 +1464,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, } }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1457,8 +1478,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -2603,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N assert { 'trade_id': 1, 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, @@ -2653,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.044e-05, @@ -2710,7 +2731,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.08801e-05, @@ -2916,7 +2937,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, @@ -3899,7 +3920,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' assert len(Trade.query.all()) == 1 @@ -4355,7 +4376,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): is_open=True, amount=20, open_rate=0.01, - exchange='bittrex', + exchange='binance', ) Trade.query.session.add(trade) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0a3d6858d..3b90f368f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -64,7 +64,7 @@ def test_init_dryrun_db(default_conf, tmpdir): @pytest.mark.usefixtures("init_persistence") -def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): +def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): """ On this test we will buy and sell a crypto currency. @@ -102,7 +102,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) assert trade.open_order_id is None assert trade.close_profit is None @@ -142,7 +142,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.utcnow().datetime, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -177,7 +177,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -205,7 +205,7 @@ def test_trade_close(limit_buy_order, limit_sell_order, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, - exchange='bittrex', + exchange='binance', ) assert trade.close_profit is None assert trade.close_date is None @@ -233,7 +233,7 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -250,7 +250,7 @@ def test_update_open_order(limit_buy_order): amount=5, fee_open=0.1, fee_close=0.1, - exchange='bittrex', + exchange='binance', ) assert trade.open_order_id is None @@ -274,7 +274,7 @@ def test_update_invalid_order(limit_buy_order): open_rate=0.001, fee_open=0.1, fee_close=0.1, - exchange='bittrex', + exchange='binance', ) limit_buy_order['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): @@ -290,7 +290,7 @@ def test_calc_open_trade_value(limit_buy_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'open_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -311,7 +311,7 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'close_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -336,7 +336,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -370,7 +370,7 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -400,7 +400,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345' ) Trade.query.session.add(trade) @@ -412,7 +412,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_sell_12345' ) Trade.query.session.add(trade) @@ -425,7 +425,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='prod_buy_12345' ) Trade.query.session.add(trade) @@ -463,7 +463,7 @@ def test_migrate_old(mocker, default_conf, fee): );""" insert_table_old = """INSERT INTO trades (exchange, pair, is_open, open_order_id, fee, open_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 1, '123123', {fee}, + VALUES ('binance', 'BTC_ETC', 1, '123123', {fee}, 0.00258580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -472,7 +472,7 @@ def test_migrate_old(mocker, default_conf, fee): ) insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee, open_rate, close_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 0, {fee}, + VALUES ('binance', 'BTC_ETC', 0, {fee}, 0.00258580, 0.00268580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -500,7 +500,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.amount_requested == amount assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" - assert trade.exchange == "bittrex" + assert trade.exchange == "binance" assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 @@ -694,7 +694,7 @@ def test_adjust_stop_loss(fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -746,7 +746,7 @@ def test_adjust_min_max_rates(fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -790,7 +790,7 @@ def test_to_json(default_conf, fee): fee_close=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345' ) result = trade.to_json() @@ -841,7 +841,7 @@ def test_to_json(default_conf, fee): 'max_rate': None, 'strategy': None, 'timeframe': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } # Simulate dry_run entries @@ -856,7 +856,7 @@ def test_to_json(default_conf, fee): close_date=arrow.utcnow().shift(hours=-1).datetime, open_rate=0.123, close_rate=0.125, - exchange='bittrex', + exchange='binance', ) result = trade.to_json() assert isinstance(result, dict) @@ -906,7 +906,7 @@ def test_to_json(default_conf, fee): 'sell_order_status': None, 'strategy': None, 'timeframe': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } @@ -919,7 +919,7 @@ def test_stoploss_reinitialization(default_conf, fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -978,7 +978,7 @@ def test_update_fee(fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -1017,7 +1017,7 @@ def test_fee_updated(fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) From 1936dd1ee8a0c3187dfec1216909c71572ae1205 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 15:45:07 +0200 Subject: [PATCH 0243/1386] Add test-case verifying "changing" wallet with unlimited amount --- tests/test_wallets.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 562957790..86f49698b 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -121,13 +121,14 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) -@pytest.mark.parametrize("balance_ratio,result1", [ - (1, 50), - (0.99, 49.5), - (0.50, 25), +@pytest.mark.parametrize("balance_ratio,result1,result2", [ + (1, 50, 66.66666), + (0.99, 49.5, 66.0), + (0.50, 25, 33.3333), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - limit_buy_order_open, fee, mocker) -> None: + result2, limit_buy_order_open, + fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -150,7 +151,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None @@ -159,6 +160,12 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) assert result == 0 + freqtrade.config['max_open_trades'] = 3 + freqtrade.config['dry_run_wallet'] = 200 + freqtrade.wallets.start_cap = 200 + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + assert round(result, 4) == round(result2, 4) + # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) From 06d6f9ac41b1425a19062074ba6a0f945a98788f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 15:55:48 +0200 Subject: [PATCH 0244/1386] Fix calculation of unlimited_stake in case of modified wallet --- freqtrade/wallets.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f4432e932..889fe6fa8 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -130,14 +130,13 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets - def _get_available_stake_amount(self) -> float: + def _get_available_stake_amount(self, val_tied_up: float) -> float: """ Return the total currently available balance in stake currency, respecting tradable_balance_ratio. Calculated as - ( + free amount ) * tradable_balance_ratio - + ( + free amount) * tradable_balance_ratio - """ - val_tied_up = Trade.total_open_trades_stakes() # Ensure % is used from the overall balance # Otherwise we'd risk lowering stakes with each open trade. @@ -151,12 +150,13 @@ class Wallets: Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. """ - if not free_open_trades: + if not free_open_trades or self._config['max_open_trades'] == 0: return 0 - available_amount = self._get_available_stake_amount() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) - return available_amount / free_open_trades + return (available_amount + val_tied_up) / self._config['max_open_trades'] def _check_available_stake_amount(self, stake_amount: float) -> float: """ @@ -165,7 +165,8 @@ class Wallets: :return: float: Stake amount :raise: DependencyException if balance is lower than stake-amount """ - available_amount = self._get_available_stake_amount() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) if self._config['amend_last_stake_amount']: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio From 71b017e7c34c837b13e039740154cd0896d7bf79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:53:16 +0200 Subject: [PATCH 0245/1386] Simplify webhook test --- tests/rpc/test_rpc_webhook.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 62818ecbb..bfb9cbb01 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -25,6 +25,11 @@ def get_webhook_dict() -> dict: "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhooksell": { "value1": "Selling {pair}", "value2": "limit {limit:8f}", @@ -35,6 +40,11 @@ def get_webhook_dict() -> dict: "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -76,8 +86,8 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) # Test buy cancel - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() + msg = { 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': 'Binance', @@ -97,8 +107,7 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) # Test sell - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() msg = { 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -123,8 +132,7 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksell"]["value3"].format(**msg)) # Test sell cancel - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'exchange': 'Binance', From fecd5c582b81e191b82a5d90834b0592eb1ffbe7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:58:29 +0200 Subject: [PATCH 0246/1386] Add buy and sell fill notifications closes #3542 --- config_full.json.example | 2 ++ docs/webhook-config.md | 46 +++++++++++++++++++++++++++++++ freqtrade/freqtradebot.py | 28 ++++++++++++++++--- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 8 ++++++ freqtrade/rpc/webhook.py | 4 +++ tests/rpc/test_rpc_webhook.py | 51 +++++++++++++++++++++++++++++++++-- 7 files changed, 136 insertions(+), 5 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 717797933..973afe2c8 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -163,7 +163,9 @@ "warning": "on", "startup": "on", "buy": "on", + "buy_fill": "on", "sell": "on", + "sell_fill": "on", "buy_cancel": "on", "sell_cancel": "on" } diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 2e41ad2cc..8ce6edc18 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -19,6 +19,11 @@ Sample configuration (tested using IFTTT). "value1": "Cancelling Open Buy Order for {pair}", "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "" }, "webhooksell": { "value1": "Selling {pair}", @@ -30,6 +35,11 @@ Sample configuration (tested using IFTTT). "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}.", + "value3": "" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -91,6 +101,21 @@ Possible parameters are: * `order_type` * `current_rate` +### Webhookbuyfill + +The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `open_rate` +* `amount` +* `open_date` +* `stake_amount` +* `stake_currency` +* `fiat_currency` + ### Webhooksell The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. @@ -103,6 +128,27 @@ Possible parameters are: * `limit` * `amount` * `open_rate` +* `profit_amount` +* `profit_ratio` +* `stake_currency` +* `fiat_currency` +* `sell_reason` +* `order_type` +* `open_date` +* `close_date` + +### Webhooksellfill + +The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `gain` +* `close_rate` +* `amount` +* `open_rate` * `current_rate` * `profit_amount` * `profit_ratio` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..68f98ec21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -675,6 +675,21 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) + def _notify_buy_fill(self, trade: Trade) -> None: + msg = { + 'trade_id': trade.id, + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'open_rate': trade.open_rate, + 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': trade.amount, + 'open_date': trade.open_date, + } + self.rpc.send_msg(msg) + # # SELL / exit positions / close trades logic and methods # @@ -1212,19 +1227,20 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_sell(self, trade: Trade, order_type: str) -> None: + def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: """ Sends rpc notification when a sell occured. """ profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. - current_rate = self.get_sell_rate(trade.pair, False) + current_rate = self.get_sell_rate(trade.pair, False) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill + else RPCMessageType.SELL_NOTIFICATION), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1233,6 +1249,7 @@ class FreqtradeBot(LoggingMixin): '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, @@ -1344,9 +1361,14 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() + elif trade.open_order_id is None: + # Buy fill + self._notify_buy_fill(trade) + return False def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..bf0b88f6c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -35,8 +35,10 @@ class RPCMessageType(Enum): WARNING_NOTIFICATION = 'warning' STARTUP_NOTIFICATION = 'startup' BUY_NOTIFICATION = 'buy' + BUY_FILL_NOTIFICATION = 'buy_fill' BUY_CANCEL_NOTIFICATION = 'buy_cancel' SELL_NOTIFICATION = 'sell' + SELL_FILL_NOTIFICATION = 'sell_fill' SELL_CANCEL_NOTIFICATION = 'sell_cancel' def __repr__(self): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 09b7b235c..4dceeb46c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -209,6 +209,10 @@ class Telegram(RPCHandler): "Cancelling open buy Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) @@ -240,6 +244,10 @@ class Telegram(RPCHandler): message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 5a30a9be8..c7e012af5 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -49,8 +49,12 @@ class Webhook(RPCHandler): valuedict = self._config['webhook'].get('webhookbuy', None) elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhookbuycancel', None) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhookbuyfill', None) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksell', None) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhooksellfill', None) elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksellcancel', None) elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index bfb9cbb01..38d2fe539 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -43,7 +43,7 @@ def get_webhook_dict() -> dict: "webhooksellfill": { "value1": "Sell Order for {pair} filled", "value2": "at {close_rate:8f}", - "value3": "{stake_amount:8f} {stake_currency}" + "value3": "" }, "webhookstatus": { "value1": "Status: {status}", @@ -59,7 +59,7 @@ def test__init__(mocker, default_conf): assert webhook._config == default_conf -def test_send_msg(default_conf, mocker): +def test_send_msg_webhook(default_conf, mocker): default_conf["webhook"] = get_webhook_dict() msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) @@ -106,6 +106,27 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) + # Test buy fill + msg_mock.reset_mock() + + msg = { + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'open_rate': 0.005, + 'stake_amount': 0.8, + 'stake_amount_fiat': 500, + 'stake_currency': 'BTC', + 'fiat_currency': 'EUR' + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) # Test sell msg_mock.reset_mock() msg = { @@ -156,6 +177,32 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) + # Test Sell fill + msg_mock.reset_mock() + msg = { + 'type': RPCMessageType.SELL_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': "profit", + 'close_rate': 0.005, + 'amount': 0.8, + 'order_type': 'limit', + 'open_rate': 0.004, + 'current_rate': 0.005, + 'profit_amount': 0.001, + 'profit_ratio': 0.20, + 'stake_currency': 'BTC', + 'sell_reason': SellType.STOP_LOSS.value + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhooksellfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhooksellfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) + for msgtype in [RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION, RPCMessageType.STARTUP_NOTIFICATION]: From 8800a097700077955bc5149d567c1df92e1f9445 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 21:32:04 +0200 Subject: [PATCH 0247/1386] Don't send double-notifications for stoploss fills --- freqtrade/freqtradebot.py | 3 ++- tests/rpc/test_rpc_telegram.py | 13 ++++++++----- tests/test_freqtradebot.py | 23 +++++++++++++++-------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 68f98ec21..76212bf97 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,7 +1361,8 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - self._notify_sell(trade, '', True) + if not stoploss_order: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ba32dc385..a3c823aac 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -683,7 +683,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -703,6 +703,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -743,7 +744,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { @@ -764,6 +765,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -794,9 +796,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - # Called for each trade 3 times - assert msg_mock.call_count == 8 - msg = msg_mock.call_args_list[1][0][0] + # Called for each trade 4 times + assert msg_mock.call_count == 12 + msg = msg_mock.call_args_list[2][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -815,6 +817,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == msg diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 433cce170..39c3f0561 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1710,6 +1710,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No open_rate=0.01, open_date=arrow.utcnow().datetime, amount=11, + exchange="binance", ) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) @@ -2262,7 +2263,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 assert open_trade.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 @@ -2319,7 +2320,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old # note this is for a partially-complete buy order freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 assert trades[0].amount == 23.0 @@ -2354,7 +2355,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap assert log_has_re(r"Applying fee on amount for Trade.*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2394,7 +2395,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, assert log_has_re(r"Could not update trade amount: .*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2639,6 +2640,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'sell_reason': SellType.ROI.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -2689,6 +2691,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'sell_reason': SellType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -2746,7 +2749,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'sell_reason': SellType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, - + 'close_rate': ANY, } == last_msg @@ -2830,7 +2833,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke trade = Trade.query.first() assert trade assert cancel_order.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, @@ -2898,7 +2901,10 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f assert trade.stoploss_order_id is None assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 + assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY_NOTIFICATION + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL_NOTIFICATION + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL_NOTIFICATION def test_execute_sell_market_order(default_conf, ticker, fee, @@ -2932,7 +2938,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert not trade.is_open assert trade.close_profit == 0.0620716 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -2952,6 +2958,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, 'sell_reason': SellType.ROI.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg From 0341ac5a55bc0a985776c38137489bc81960e80a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 06:41:58 +0200 Subject: [PATCH 0248/1386] rename RPC message types --- freqtrade/freqtradebot.py | 16 ++--- freqtrade/rpc/rpc.py | 18 ++--- freqtrade/rpc/rpc_manager.py | 8 +-- freqtrade/rpc/telegram.py | 117 +++++++++++++++++---------------- freqtrade/rpc/webhook.py | 18 ++--- tests/rpc/test_rpc_manager.py | 6 +- tests/rpc/test_rpc_telegram.py | 28 ++++---- tests/rpc/test_rpc_webhook.py | 28 ++++---- tests/test_freqtradebot.py | 14 ++-- 9 files changed, 128 insertions(+), 125 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 76212bf97..c48ea851e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -113,7 +113,7 @@ class FreqtradeBot(LoggingMixin): via RPC about changes in the bot status. """ self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': msg }) @@ -205,7 +205,7 @@ class FreqtradeBot(LoggingMixin): if len(open_trades) != 0: msg = { - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': f"{len(open_trades)} open trades active.\n\n" f"Handle these trades manually on {self.exchange.name}, " f"or '/start' the bot again and use '/stopbuy' " @@ -634,7 +634,7 @@ class FreqtradeBot(LoggingMixin): """ msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -658,7 +658,7 @@ class FreqtradeBot(LoggingMixin): msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -678,7 +678,7 @@ class FreqtradeBot(LoggingMixin): def _notify_buy_fill(self, trade: Trade) -> None: msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'type': RPCMessageType.BUY_FILL, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'open_rate': trade.open_rate, @@ -1239,8 +1239,8 @@ class FreqtradeBot(LoggingMixin): gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill - else RPCMessageType.SELL_NOTIFICATION), + 'type': (RPCMessageType.SELL_FILL if fill + else RPCMessageType.SELL), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1284,7 +1284,7 @@ class FreqtradeBot(LoggingMixin): gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index bf0b88f6c..e5c0dffba 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -31,15 +31,15 @@ logger = logging.getLogger(__name__) class RPCMessageType(Enum): - STATUS_NOTIFICATION = 'status' - WARNING_NOTIFICATION = 'warning' - STARTUP_NOTIFICATION = 'startup' - BUY_NOTIFICATION = 'buy' - BUY_FILL_NOTIFICATION = 'buy_fill' - BUY_CANCEL_NOTIFICATION = 'buy_cancel' - SELL_NOTIFICATION = 'sell' - SELL_FILL_NOTIFICATION = 'sell_fill' - SELL_CANCEL_NOTIFICATION = 'sell_cancel' + STATUS = 'status' + WARNING = 'warning' + STARTUP = 'startup' + BUY = 'buy' + BUY_FILL = 'buy_fill' + BUY_CANCEL = 'buy_cancel' + SELL = 'sell' + SELL_FILL = 'sell_fill' + SELL_CANCEL = 'sell_cancel' def __repr__(self): return self.value diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 7977d68de..f819b55b4 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -67,7 +67,7 @@ class RPCManager: def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': 'Dry run is enabled. All trades are simulated.' }) stake_currency = config['stake_currency'] @@ -79,7 +79,7 @@ class RPCManager: exchange_name = config['exchange']['name'] strategy_name = config.get('strategy', '') self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'*Exchange:* `{exchange_name}`\n' f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' f'*Minimum ROI:* `{minimal_roi}`\n' @@ -88,13 +88,13 @@ class RPCManager: f'*Strategy:* `{strategy_name}`' }) self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'Searching for {stake_currency} pairs to buy and sell ' f'based on {pairlist.short_desc()}' }) if len(protections.name_list) > 0: prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()]) self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'Using Protections: \n{prots}' }) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4dceeb46c..778baea3c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -176,6 +176,53 @@ class Telegram(RPCHandler): """ self._updater.stop() + def _format_buy_msg(self, msg: Dict[str, Any]) -> str: + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) + else: + msg['stake_amount_fiat'] = 0 + + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" + f" (#{msg['trade_id']})\n" + f"*Amount:* `{msg['amount']:.8f}`\n" + f"*Open Rate:* `{msg['limit']:.8f}`\n" + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") + + if msg.get('fiat_currency', None): + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + message += ")`" + return message + + def _format_sell_msg(self, msg: Dict[str, Any]) -> str: + msg['amount'] = round(msg['amount'], 8) + msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) + msg['duration'] = msg['close_date'].replace( + microsecond=0) - msg['open_date'].replace(microsecond=0) + msg['duration_min'] = msg['duration'].total_seconds() / 60 + + msg['emoji'] = self._get_sell_emoji(msg) + + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Profit:* `{profit_percent:.2f}%`").format(**msg) + + # Check if all sell properties are available. + # This might not be the case if the message origin is triggered by /forcesell + if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) + and self._rpc._fiat_converter): + msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) + message += (' `({gain}: {profit_amount:.8f} {stake_currency}' + ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) + return message + def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ @@ -186,75 +233,31 @@ class Telegram(RPCHandler): # Notification disabled return - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: - if self._rpc._fiat_converter: - msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( - msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) - else: - msg['stake_amount_fiat'] = 0 + if msg['type'] == RPCMessageType.BUY: + message = self._format_buy_msg(msg) - message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" - f" (#{msg['trade_id']})\n" - f"*Amount:* `{msg['amount']:.8f}`\n" - f"*Open Rate:* `{msg['limit']:.8f}`\n" - f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") - - if msg.get('fiat_currency', None): - message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" - message += ")`" - - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): + msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell' message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling open buy Order for {pair} (#{trade_id}). " + "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + elif msg['type'] == (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): + msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' + message = ("\N{LARGE CIRCLE} *{exchange}:* " "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: - msg['amount'] = round(msg['amount'], 8) - msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) - msg['duration'] = msg['close_date'].replace( - microsecond=0) - msg['open_date'].replace(microsecond=0) - msg['duration_min'] = msg['duration'].total_seconds() / 60 + elif msg['type'] == RPCMessageType.SELL: + message = self._format_sell_msg(msg) - msg['emoji'] = self._get_sell_emoji(msg) - - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) - - # Check if all sell properties are available. - # This might not be the case if the message origin is triggered by /forcesell - if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) - and self._rpc._fiat_converter): - msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( - msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - message += (' `({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) - - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: - message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " - "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) - - elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: - message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) - - elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: + elif msg['type'] == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: + elif msg['type'] == RPCMessageType.WARNING: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION: + elif msg['type'] == RPCMessageType.STARTUP: message = '{status}'.format(**msg) else: diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index c7e012af5..24e1348f1 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -45,21 +45,21 @@ class Webhook(RPCHandler): """ Send a message to telegram channel """ try: - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: + if msg['type'] == RPCMessageType.BUY: valuedict = self._config['webhook'].get('webhookbuy', None) - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + elif msg['type'] == RPCMessageType.BUY_CANCEL: valuedict = self._config['webhook'].get('webhookbuycancel', None) - elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + elif msg['type'] == RPCMessageType.BUY_FILL: valuedict = self._config['webhook'].get('webhookbuyfill', None) - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL: valuedict = self._config['webhook'].get('webhooksell', None) - elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL_FILL: valuedict = self._config['webhook'].get('webhooksellfill', None) - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL_CANCEL: valuedict = self._config['webhook'].get('webhooksellcancel', None) - elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.STARTUP_NOTIFICATION, - RPCMessageType.WARNING_NOTIFICATION): + elif msg['type'] in (RPCMessageType.STATUS, + RPCMessageType.STARTUP, + RPCMessageType.WARNING): valuedict = self._config['webhook'].get('webhookstatus', None) else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 6996c932b..69a757fcf 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -71,7 +71,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'test' }) @@ -86,7 +86,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'test' }) @@ -124,7 +124,7 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] - rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION, + rpc_manager.send_msg({'type': RPCMessageType.STARTUP, 'status': 'TestMessage'}) assert log_has( "Message type 'startup' not implemented by handler webhook.", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a3c823aac..accb94d34 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -686,7 +686,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -748,7 +748,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, last_msg = msg_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -800,7 +800,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None assert msg_mock.call_count == 12 msg = msg_mock.call_args_list[2][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1198,7 +1198,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1243,7 +1243,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1261,7 +1261,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: old_convamount = telegram._rpc._fiat_converter.convert_amount telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1291,7 +1291,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1328,7 +1328,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: old_convamount = telegram._rpc._fiat_converter.convert_amount telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1340,7 +1340,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1357,7 +1357,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'running' }) assert msg_mock.call_args[0][0] == '*Status:* `running`' @@ -1366,7 +1366,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: def test_warning_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': 'message' }) assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' @@ -1375,7 +1375,7 @@ def test_warning_notification(default_conf, mocker) -> None: def test_startup_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': '*Custom:* `Hello World`' }) assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' @@ -1394,7 +1394,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1420,7 +1420,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 38d2fe539..0560f8d53 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -68,7 +68,7 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, @@ -89,7 +89,7 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock.reset_mock() msg = { - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, @@ -110,8 +110,8 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock.reset_mock() msg = { - 'type': RPCMessageType.BUY_FILL_NOTIFICATION, - 'exchange': 'Bittrex', + 'type': RPCMessageType.BUY_FILL, + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'open_rate': 0.005, 'stake_amount': 0.8, @@ -130,7 +130,7 @@ def test_send_msg_webhook(default_conf, mocker): # Test sell msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", @@ -155,7 +155,7 @@ def test_send_msg_webhook(default_conf, mocker): # Test sell cancel msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", @@ -180,8 +180,8 @@ def test_send_msg_webhook(default_conf, mocker): # Test Sell fill msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_FILL_NOTIFICATION, - 'exchange': 'Bittrex', + 'type': RPCMessageType.SELL_FILL, + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'close_rate': 0.005, @@ -203,9 +203,9 @@ def test_send_msg_webhook(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) - for msgtype in [RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.WARNING_NOTIFICATION, - RPCMessageType.STARTUP_NOTIFICATION]: + for msgtype in [RPCMessageType.STATUS, + RPCMessageType.WARNING, + RPCMessageType.STARTUP]: # Test notification msg = { 'type': msgtype, @@ -228,8 +228,8 @@ def test_exception_send_msg(default_conf, mocker, caplog): del default_conf["webhook"]["webhookbuy"] webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) - assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks", + webhook.send_msg({'type': RPCMessageType.BUY}) + assert log_has(f"Message type '{RPCMessageType.BUY}' not configured for webhooks", caplog) default_conf["webhook"] = get_webhook_dict() @@ -238,7 +238,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 39c3f0561..25239d503 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2624,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'trade_id': 1, - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', @@ -2674,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -2732,7 +2732,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -2902,9 +2902,9 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 3 - assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY_NOTIFICATION - assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL_NOTIFICATION - assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL_NOTIFICATION + assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL def test_execute_sell_market_order(default_conf, ticker, fee, @@ -2941,7 +2941,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', From d740aae8ca96a221a744b69fb16f708b8a632623 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 06:49:29 +0200 Subject: [PATCH 0249/1386] Default fill notifications to off --- freqtrade/constants.py | 14 ++++++++++++-- freqtrade/freqtradebot.py | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 7b955c37d..aea6e1ff2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -246,14 +246,24 @@ CONF_SCHEMA = { 'balance_dust_level': {'type': 'number', 'minimum': 0.0}, 'notification_settings': { 'type': 'object', + 'default': {}, 'properties': { 'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, - 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, - 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS} + 'buy_fill': {'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, + 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_fill': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, } } }, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c48ea851e..ad55b38f8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,12 +1361,12 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - if not stoploss_order: + if not stoploss_order and not trade.open_order_id: self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() - elif trade.open_order_id is None: + elif not trade.open_order_id: # Buy fill self._notify_buy_fill(trade) From efbe0843be6a05d623a77c8e91adfce2158ca9f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 07:57:34 +0200 Subject: [PATCH 0250/1386] Add documentation for fill messages --- docs/telegram-usage.md | 9 ++++++++- tests/rpc/test_rpc_telegram.py | 8 ++++---- tests/test_freqtradebot.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 377977892..824cb17c7 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -82,12 +82,19 @@ Example configuration showing the different settings: "buy": "silent", "sell": "on", "buy_cancel": "silent", - "sell_cancel": "on" + "sell_cancel": "on", + "buy_fill": "off", + "sell_fill": "off" }, "balance_dust_level": 0.01 }, ``` +`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. +`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange. +`*_fill` notifications are off by default and must be explicitly enabled. + + `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. ## Create a custom keyboard (command shortcut buttons) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index accb94d34..718c1d3a0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1335,8 +1335,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'Cancelled on exchange' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' - ' Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' + ' Reason: Cancelled on exchange.') msg_mock.reset_mock() telegram.send_msg({ @@ -1347,8 +1347,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'timeout' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' - ' Reason: timeout') + == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' + ' Reason: timeout.') # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 25239d503..44791f928 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2263,7 +2263,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 assert open_trade.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 From f821ef5aec8fb8321b75bfbe9ab33e0eed380ef5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 19:36:30 +0200 Subject: [PATCH 0251/1386] Final finetunings of rpc_fill messages --- freqtrade/rpc/telegram.py | 2 +- tests/conftest.py | 3 ++- tests/rpc/test_rpc_telegram.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 778baea3c..ffe7a7ceb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -242,7 +242,7 @@ class Telegram(RPCHandler): "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): + elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' message = ("\N{LARGE CIRCLE} *{exchange}:* " diff --git a/tests/conftest.py b/tests/conftest.py index cc4fe91f0..788586134 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -314,7 +314,8 @@ def get_default_conf(testdatadir): "telegram": { "enabled": True, "token": "token", - "chat_id": "0" + "chat_id": "0", + "notification_settings": {}, }, "datadir": str(testdatadir), "initial_state": "running", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 718c1d3a0..d72ba36ad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1254,6 +1254,25 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'Reason: cancelled due to timeout.') +def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: + + default_conf['telegram']['notification_settings']['buy_fill'] = 'on' + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + telegram.send_msg({ + 'type': RPCMessageType.BUY_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/USDT', + 'open_rate': 200, + 'stake_amount': 100, + 'amount': 0.5, + 'open_date': arrow.utcnow().datetime + }) + assert (msg_mock.call_args[0][0] == '\N{LARGE CIRCLE} *Binance:* ' + 'Buy order for ETH/USDT (#1) filled for 200.') + + def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) From fd110c7d625fc8b5bc12445fe94998eb1f10eb31 Mon Sep 17 00:00:00 2001 From: Jose Hidalgo Date: Tue, 20 Apr 2021 11:50:53 -0600 Subject: [PATCH 0252/1386] The error that it prints says the contrary to what was evaluated. ex. Trading stopped due to Max Drawdown 0.79 < 0.2 within 48 candles --- freqtrade/plugins/protections/max_drawdown_protection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index d1c6b192d..67e204039 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -61,7 +61,7 @@ class MaxDrawdown(IProtection): if drawdown > self._max_allowed_drawdown: self.log_once( - f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}" + f"Trading stopped due to Max Drawdown {drawdown:.2f} > {self._max_allowed_drawdown}" f" within {self.lookback_period_str}.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) From 5defd9a7f882d75a9bf2c457cdc3ddd94a8bb7e4 Mon Sep 17 00:00:00 2001 From: Bernd Zeimetz Date: Tue, 20 Apr 2021 19:52:57 +0200 Subject: [PATCH 0253/1386] setup.sh: Install libpython3-dev on Debian/Ubuntu Python.h is required to build c modules for Python. --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index d0ca1f643..631c31df2 100755 --- a/setup.sh +++ b/setup.sh @@ -138,7 +138,7 @@ function install_macos() { # Install bot Debian_ubuntu function install_debian() { sudo apt-get update - sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git + sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git libpython3-dev install_talib } From cfa9315e2a3dd1706bca0f09a4e1e48315912ec3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 20:29:53 +0200 Subject: [PATCH 0254/1386] Prevent out of candle ROI sells --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff1dd934c..a1d4a2578 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -239,7 +239,7 @@ class Backtesting: # Use the maximum between close_rate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. - return max(close_rate, sell_row[LOW_IDX]) + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) else: # This should not be reached... From bd92ce938c6b92c2092cd119b597fc29d26ec2ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Apr 2021 16:05:28 +0200 Subject: [PATCH 0255/1386] trade_history should paginate through results this avoids huge results --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 4 ++-- freqtrade/rpc/rpc.py | 10 ++++++---- scripts/rest_client.py | 14 ++++++++++---- tests/rpc/test_rpc_apiserver.py | 2 +- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 12bee1cf2..e582f6aa8 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -199,6 +199,7 @@ class OpenTradeSchema(TradeSchema): class TradeResponse(BaseModel): trades: List[TradeSchema] trades_count: int + total_trades: int class ForceBuyResponse(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index ebfafc290..cb3a5a710 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -85,8 +85,8 @@ def status(rpc: RPC = Depends(get_rpc)): # Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s) # on big databases. Correct response model: response_model=TradeResponse, @router.get('/trades', tags=['info', 'trading']) -def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_history(limit) +def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_trade_history(min(limit, 500), offset=offset, order_by_id=True) @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e5c0dffba..a7a4dcf5c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -300,11 +300,12 @@ class RPC: 'data': data } - def _rpc_trade_history(self, limit: int) -> Dict: + def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ - if limit > 0: + order_by = Trade.id if order_by_id else Trade.close_date.desc() + if limit: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - Trade.close_date.desc()).limit(limit) + order_by).limit(limit).offset(offset) else: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( Trade.close_date.desc()).all() @@ -313,7 +314,8 @@ class RPC: return { "trades": output, - "trades_count": len(output) + "trades_count": len(output), + "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), } def _rpc_stats(self) -> Dict[str, Any]: diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 40b338ce8..900b784f2 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -200,13 +200,19 @@ class FtRestClient(): """ return self._get("logs", params={"limit": limit} if limit else 0) - def trades(self, limit=None): - """Return trades history. + def trades(self, limit=None, offset=None): + """Return trades history, sorted by id - :param limit: Limits trades to the X last trades. No limit to get all the trades. + :param limit: Limits trades to the X last trades. Max 500 trades. + :param offset: Offset by this amount of trades. :return: json object """ - return self._get("trades", params={"limit": limit} if limit else 0) + params = {} + if limit: + params['limit'] = limit + if offset: + params['offset'] = offset + return self._get("trades", params) def trade(self, trade_id): """Return specific trade diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 6505629eb..d87d4e59d 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -506,7 +506,7 @@ def test_api_trades(botclient, mocker, fee, markets): ) rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) - assert len(rc.json()) == 2 + assert len(rc.json()) == 3 assert rc.json()['trades_count'] == 0 create_mock_trades(fee) From 759bbd8e72fb72507669b1c67671eb3c67a95c64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:23:37 +0200 Subject: [PATCH 0256/1386] Update documentation about pagination --- docs/rest-api.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 5c25e9eeb..514e0f719 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -124,7 +124,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `stop` | Stops the trader. | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `reload_config` | Reloads the configuration file. -| `trades` | List last trades. +| `trades` | List last trades. Limited to 500 trades per call. | `trade/` | Get specific trade. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. @@ -280,9 +280,10 @@ trade :param trade_id: Specify which trade to get. trades - Return trades history. + Return trades history, sorted by id - :param limit: Limits trades to the X last trades. No limit to get all the trades. + :param limit: Limits trades to the X last trades. Max 500 trades. + :param offset: Offset by this amount of trades. version Return the version of the bot. @@ -290,6 +291,7 @@ version whitelist Show the current whitelist. + ``` ### OpenAPI interface From 05ce3acc466dda62f7f65bb80d172d972d5923a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:28:13 +0200 Subject: [PATCH 0257/1386] Improve tests for api_trades --- docs/rest-api.md | 2 -- tests/rpc/test_rpc_apiserver.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 514e0f719..a0029a44c 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -290,8 +290,6 @@ version whitelist Show the current whitelist. - - ``` ### OpenAPI interface diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d87d4e59d..69d312e65 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -508,6 +508,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert_response(rc) assert len(rc.json()) == 3 assert rc.json()['trades_count'] == 0 + assert rc.json()['total_trades'] == 0 create_mock_trades(fee) Trade.query.session.flush() @@ -516,10 +517,12 @@ def test_api_trades(botclient, mocker, fee, markets): assert_response(rc) assert len(rc.json()['trades']) == 2 assert rc.json()['trades_count'] == 2 + assert rc.json()['total_trades'] == 2 rc = client_get(client, f"{BASE_URI}/trades?limit=1") assert_response(rc) assert len(rc.json()['trades']) == 1 assert rc.json()['trades_count'] == 1 + assert rc.json()['total_trades'] == 2 def test_api_trade_single(botclient, mocker, fee, ticker, markets): From 9f6f3e0862b27693706ecbb44be0856834d05717 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:41:18 +0200 Subject: [PATCH 0258/1386] Address ZeroDivisionExceptiond closes #4764 closes #4617 --- freqtrade/persistence/models.py | 2 ++ tests/test_persistence.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 49d3e2d62..e7fd488c7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -547,6 +547,8 @@ class LocalTrade(): rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) + if self.open_trade_value == 0.0: + return 0.0 profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3b90f368f..dad0e275e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -388,6 +388,9 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): # Test with a custom fee rate on the close trade assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 + trade.open_trade_value = 0.0 + assert trade.calc_profit_ratio(fee=0.003) == 0.0 + @pytest.mark.usefixtures("init_persistence") def test_clean_dry_run_db(default_conf, fee): From 0233aa248e6fc9edfaa9567e7dd50941616d4d0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 17:22:16 +0200 Subject: [PATCH 0259/1386] Limit stake_amount to max available amount --- freqtrade/wallets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 889fe6fa8..4415e4d53 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -156,7 +156,9 @@ class Wallets: val_tied_up = Trade.total_open_trades_stakes() available_amount = self._get_available_stake_amount(val_tied_up) - return (available_amount + val_tied_up) / self._config['max_open_trades'] + # Theoretical amount can be above available amount - therefore limit to available amount! + return min((available_amount + val_tied_up) / self._config['max_open_trades'], + available_amount) def _check_available_stake_amount(self, stake_amount: float) -> float: """ From ba2d4d4656d2ecc929bd517786a2c0e325ea7c45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 19:27:21 +0200 Subject: [PATCH 0260/1386] Reduce number of calls to `Trade.total_open_traes_stakes()` --- freqtrade/wallets.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4415e4d53..dba16cc35 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,7 +145,8 @@ class Wallets: self._config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float: + def _calculate_unlimited_stake_amount(self, free_open_trades: int, available_amount: float, + val_tied_up: float) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. @@ -153,22 +154,17 @@ class Wallets: if not free_open_trades or self._config['max_open_trades'] == 0: return 0 - val_tied_up = Trade.total_open_trades_stakes() - available_amount = self._get_available_stake_amount(val_tied_up) - + possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] # Theoretical amount can be above available amount - therefore limit to available amount! - return min((available_amount + val_tied_up) / self._config['max_open_trades'], - available_amount) + return min(possible_stake, available_amount) - def _check_available_stake_amount(self, stake_amount: float) -> float: + def _check_available_stake_amount(self, stake_amount: float, available_amount: float) -> float: """ Check if stake amount can be fulfilled with the available balance for the stake currency :return: float: Stake amount :raise: DependencyException if balance is lower than stake-amount """ - val_tied_up = Trade.total_open_trades_stakes() - available_amount = self._get_available_stake_amount(val_tied_up) if self._config['amend_last_stake_amount']: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio @@ -195,17 +191,20 @@ class Wallets: stake_amount: float # Ensure wallets are uptodate. self.update() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) if edge: stake_amount = edge.stake_amount( pair, self.get_free(self._config['stake_currency']), self.get_total(self._config['stake_currency']), - Trade.total_open_trades_stakes() + val_tied_up ) else: stake_amount = self._config['stake_amount'] if stake_amount == UNLIMITED_STAKE_AMOUNT: - stake_amount = self._calculate_unlimited_stake_amount(free_open_trades) + stake_amount = self._calculate_unlimited_stake_amount( + free_open_trades, available_amount, val_tied_up) - return self._check_available_stake_amount(stake_amount) + return self._check_available_stake_amount(stake_amount, available_amount) From f7a4331c864bafc1d8454dfe7dbcb42393275768 Mon Sep 17 00:00:00 2001 From: onerobotband Date: Wed, 21 Apr 2021 18:38:57 +0100 Subject: [PATCH 0261/1386] Create config_ftx.json.example to stop the dl trades error from popping up all the time --- config_ftx.json.example | 99 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 config_ftx.json.example diff --git a/config_ftx.json.example b/config_ftx.json.example new file mode 100644 index 000000000..33e884976 --- /dev/null +++ b/config_ftx.json.example @@ -0,0 +1,99 @@ +{ + "max_open_trades": 3, + "stake_currency": "BTC", + "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "timeframe": "5m", + "dry_run": true, + "cancel_open_orders_on_exit": false, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, + "bid_strategy": { + "ask_last_balance": 0.0, + "use_order_book": false, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy": { + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 1, + "use_sell_signal": true, + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false + }, + "exchange": { + "name": "ftx", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 50 + }, + "pair_whitelist": [ + "ALGO/BTC", + "ATOM/BTC", + "BAT/BTC", + "BCH/BTC", + "BRD/BTC", + "EOS/BTC", + "ETH/BTC", + "IOTA/BTC", + "LINK/BTC", + "LTC/BTC", + "NEO/BTC", + "NXS/BTC", + "XMR/BTC", + "XRP/BTC", + "XTZ/BTC" + ], + "pair_blacklist": [ + "BNB/BTC" + ] + }, + "pairlists": [ + {"method": "StaticPairList"} + ], + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "jwt_secret_key": "somethingrandom", + "CORS_origins": [], + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} From d8c8a8d8c22923c301f8db7e1099a5422c5fe026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 20:01:10 +0200 Subject: [PATCH 0262/1386] Remvoe pointless arguments from get_trade_stake_amount --- freqtrade/freqtradebot.py | 3 +-- freqtrade/optimize/backtesting.py | 8 +++----- freqtrade/rpc/rpc.py | 3 +-- freqtrade/wallets.py | 8 ++++---- tests/optimize/test_backtesting.py | 10 +++++----- tests/test_freqtradebot.py | 16 ++++++---------- tests/test_integration.py | 6 ++---- tests/test_wallets.py | 12 ++++++------ 8 files changed, 28 insertions(+), 38 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..f370ff34f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -472,8 +472,7 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: - stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), - self.edge) + stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) if not stake_amount: logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff1dd934c..71110b914 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,11 +273,9 @@ class Backtesting: return None - def _enter_trade(self, pair: str, row: List, max_open_trades: int, - open_trade_count: int) -> Optional[LocalTrade]: + def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: try: - stake_amount = self.wallets.get_trade_stake_amount( - pair, max_open_trades - open_trade_count, None) + stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) @@ -388,7 +386,7 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): - trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start) + trade = self._enter_trade(pair, row) if trade: # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..eedb1c510 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -603,8 +603,7 @@ class RPC: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount - stakeamount = self._freqtrade.wallets.get_trade_stake_amount( - pair, self._freqtrade.get_free_open_trades()) + stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index dba16cc35..bbbe5ba5e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,13 +145,13 @@ class Wallets: self._config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self, free_open_trades: int, available_amount: float, + def _calculate_unlimited_stake_amount(self, available_amount: float, val_tied_up: float) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. """ - if not free_open_trades or self._config['max_open_trades'] == 0: + if self._config['max_open_trades'] == 0: return 0 possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] @@ -182,7 +182,7 @@ class Wallets: return stake_amount - def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float: + def get_trade_stake_amount(self, pair: str, edge=None) -> float: """ Calculate stake amount for the trade :return: float: Stake amount @@ -205,6 +205,6 @@ class Wallets: stake_amount = self._config['stake_amount'] if stake_amount == UNLIMITED_STAKE_AMOUNT: stake_amount = self._calculate_unlimited_stake_amount( - free_open_trades, available_amount, val_tied_up) + available_amount, val_tied_up) return self._check_available_stake_amount(stake_amount, available_amount) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4bbfe8a78..41d4207c3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -457,7 +457,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) -def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: +def test_backtest__enter_trade(default_conf, fee, mocker) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) @@ -474,24 +474,24 @@ def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: 0.00099, # Low 0.0012, # High ] - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2) + trade = backtesting._enter_trade(pair, row=row) assert trade is None # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert trade is None # Stake-amount too high! mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException) - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert trade is None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 433cce170..b0fa2d6c2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -160,8 +160,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: freqtrade = FreqtradeBot(default_conf) - result = freqtrade.wallets.get_trade_stake_amount( - 'ETH/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') assert result == default_conf['stake_amount'] @@ -197,14 +196,12 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b if expected[i] is not None: limit_buy_order_open['id'] = str(i) - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', - freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') assert pytest.approx(result) == expected[i] freqtrade.execute_buy('ETH/BTC', result) else: with pytest.raises(DependencyException): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', - freqtrade.get_free_open_trades()) + freqtrade.wallets.get_trade_stake_amount('ETH/BTC') def test_edge_called_in_process(mocker, edge_conf) -> None: @@ -230,9 +227,9 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: freqtrade = FreqtradeBot(edge_conf) assert freqtrade.wallets.get_trade_stake_amount( - 'NEO/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 + 'NEO/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 assert freqtrade.wallets.get_trade_stake_amount( - 'LTC/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 + 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: @@ -448,8 +445,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), - freqtrade.edge) == 0 + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, diff --git a/tests/test_integration.py b/tests/test_integration.py index 1c60faa7b..be0dd1137 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -177,8 +177,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.query.all() assert len(trades) == 4 - assert freqtrade.wallets.get_trade_stake_amount( - 'XRP/BTC', freqtrade.get_free_open_trades()) == result1 + assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1 rpc._rpc_forcebuy('TKN/BTC', None) @@ -199,8 +198,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc # One trade sold assert len(trades) == 4 # stake-amount should now be reduced, since one trade was sold at a loss. - assert freqtrade.wallets.get_trade_stake_amount( - 'XRP/BTC', freqtrade.get_free_open_trades()) < result1 + assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') < result1 # Validate that balance of sold trade is not in dry-run balances anymore. bals2 = freqtrade.wallets.get_all_balances() assert bals != bals2 diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 86f49698b..ff303e2ec 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -118,7 +118,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + freqtrade.wallets.get_trade_stake_amount('ETH/BTC') @pytest.mark.parametrize("balance_ratio,result1,result2", [ @@ -145,28 +145,28 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r freqtrade = get_patched_freqtradebot(mocker, conf) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT') assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') assert result == 0 freqtrade.config['max_open_trades'] = 3 freqtrade.config['dry_run_wallet'] = 200 freqtrade.wallets.start_cap = 200 - result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') assert round(result, 4) == round(result2, 4) # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 - result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT') assert result == 0 From 92a2e254af2c0182840830f3c0f2b9ba10d566cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 20:17:30 +0200 Subject: [PATCH 0263/1386] Fix backtesting test --- tests/optimize/test_backtesting.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 41d4207c3..00114be5b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -463,6 +463,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) default_conf['stake_amount'] = 'unlimited' + default_conf['max_open_trades'] = 2 backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' row = [ @@ -478,8 +479,14 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 + # Fake 2 trades, so there's not enough amount for the next trade left. + LocalTrade.trades_open.append(trade) + LocalTrade.trades_open.append(trade) trade = backtesting._enter_trade(pair, row=row) assert trade is None + LocalTrade.trades_open.pop() + trade = backtesting._enter_trade(pair, row=row) + assert trade is not None # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) @@ -487,7 +494,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: trade = backtesting._enter_trade(pair, row=row) assert trade is None - # Stake-amount too high! + # Stake-amount throwing error mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException) From 896ec58cadd3d94eb8d61077e3ec07be1c895b48 Mon Sep 17 00:00:00 2001 From: Jose Hidalgo Date: Wed, 21 Apr 2021 15:02:33 -0600 Subject: [PATCH 0264/1386] Add the reason why there is a global pairlock when lock is available --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ad55b38f8..a64d3f4cc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -378,7 +378,7 @@ class FreqtradeBot(LoggingMixin): if lock: self.log_once(f"Global pairlock active until " f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. " - "Not creating new trades.", logger.info) + f"Not creating new trades, reason: {lock.reason}.", logger.info) else: self.log_once("Global pairlock active. Not creating new trades.", logger.info) return trades_created From 515c73f3990ee976baea1c8dd959a090d64a2c86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 06:51:26 +0200 Subject: [PATCH 0265/1386] Don't hard-limit trades endpoint for now --- freqtrade/rpc/api_server/api_v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index cb3a5a710..e907b92f0 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -86,7 +86,7 @@ def status(rpc: RPC = Depends(get_rpc)): # on big databases. Correct response model: response_model=TradeResponse, @router.get('/trades', tags=['info', 'trading']) def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_history(min(limit, 500), offset=offset, order_by_id=True) + return rpc._rpc_trade_history(limit, offset=offset, order_by_id=True) @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) From 09efa7b06b7cd39726d07c9f1f231249651271c8 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 10:07:13 +0300 Subject: [PATCH 0266/1386] Add --new-pairs-days parameter for download-data command. This parameter allows us to customize a number of days we would like to download for new pairs only. This allows us to achieve efficient data update, downloading all data for new pairs and only missing data for existing pairs. To do that use `freqtrade download-data --new-pairs-days=3650` (not specifying `--days` or `--timerange` causes freqtrade to download only missing data for existing pairs). --- docs/data-download.md | 6 ++++-- freqtrade/commands/arguments.py | 5 +++-- freqtrade/commands/cli_options.py | 7 +++++++ freqtrade/commands/data_commands.py | 9 +++++---- freqtrade/configuration/configuration.py | 7 +++++++ freqtrade/data/history/history_utils.py | 15 ++++++++++----- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 7a78334d5..7b09cf49c 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -11,8 +11,9 @@ Otherwise `--exchange` becomes mandatory. You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used. !!! Tip "Tip: Updating existing data" - If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. - Be careful though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. + If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data. + If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only. + If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data. ### Usage @@ -34,6 +35,7 @@ optional arguments: separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. + --new-pairs-days INT Download data of new pairs for given number of days. Default: `30`. --timerange TIMERANGE Specify what timerange of data to use. --dl-trades Download trades instead of OHLCV data. The bot will diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9cf9992ce..ffd317799 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -60,8 +60,9 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] -ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "timerange", "download_trades", "exchange", - "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] +ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange", + "download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv", + "dataformat_trades"] ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index e49895de4..80c56ecfa 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -345,6 +345,13 @@ AVAILABLE_CLI_OPTIONS = { type=check_int_positive, metavar='INT', ), + "new_pairs_days": Arg( + '--new-pairs-days', + help='Download data of new pairs for given number of days. Default: `%(default)s`.', + type=check_int_positive, + metavar='INT', + default=30, + ), "download_trades": Arg( '--dl-trades', help='Download trades instead of OHLCV data. The bot will resample trades to the ' diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 1ce02eee5..58191ddb4 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -62,8 +62,8 @@ def start_download_data(args: Dict[str, Any]) -> None: if config.get('download_trades'): pairs_not_available = refresh_backtest_trades_data( exchange, pairs=expanded_pairs, datadir=config['datadir'], - timerange=timerange, erase=bool(config.get('erase')), - data_format=config['dataformat_trades']) + timerange=timerange, new_pairs_days=config['new_pairs_days'], + erase=bool(config.get('erase')), data_format=config['dataformat_trades']) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( @@ -75,8 +75,9 @@ def start_download_data(args: Dict[str, Any]) -> None: else: pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=expanded_pairs, timeframes=config['timeframes'], - datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), - data_format=config['dataformat_ohlcv']) + datadir=config['datadir'], timerange=timerange, + new_pairs_days=config['new_pairs_days'], + erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv']) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index cc11f97c2..86f337c1b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -108,6 +108,8 @@ class Configuration: self._process_plot_options(config) + self._process_data_options(config) + # Check if the exchange set by the user is supported check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) @@ -399,6 +401,11 @@ class Configuration: self._args_to_config(config, argname='dataformat_trades', logstring='Using "{}" to store trades data.') + def _process_data_options(self, config: Dict[str, Any]) -> None: + + self._args_to_config(config, argname='new_pairs_days', + logstring='Detected --new-pairs-days: {}') + def _process_runmode(self, config: Dict[str, Any]) -> None: self._args_to_config(config, argname='dry_run', diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 3b8b5a2f0..58965abe0 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -155,6 +155,7 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona def _download_pair_history(datadir: Path, exchange: Exchange, pair: str, *, + new_pairs_days: int = 30, timeframe: str = '5m', timerange: Optional[TimeRange] = None, data_handler: IDataHandler = None) -> bool: @@ -193,7 +194,7 @@ def _download_pair_history(datadir: Path, timeframe=timeframe, since_ms=since_ms if since_ms else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000 + days=-new_pairs_days).float_timestamp) * 1000 ) # TODO: Maybe move parsing to exchange class (?) new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair, @@ -223,7 +224,8 @@ def _download_pair_history(datadir: Path, def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str], datadir: Path, timerange: Optional[TimeRange] = None, - erase: bool = False, data_format: str = None) -> List[str]: + new_pairs_days: int = 30, erase: bool = False, + data_format: str = None) -> List[str]: """ Refresh stored ohlcv data for backtesting and hyperopt operations. Used by freqtrade download-data subcommand. @@ -246,12 +248,14 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes logger.info(f'Downloading pair {pair}, interval {timeframe}.') _download_pair_history(datadir=datadir, exchange=exchange, pair=pair, timeframe=str(timeframe), + new_pairs_days=new_pairs_days, timerange=timerange, data_handler=data_handler) return pairs_not_available def _download_trades_history(exchange: Exchange, pair: str, *, + new_pairs_days: int = 30, timerange: Optional[TimeRange] = None, data_handler: IDataHandler ) -> bool: @@ -263,7 +267,7 @@ def _download_trades_history(exchange: Exchange, since = timerange.startts * 1000 if \ (timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000 + days=-new_pairs_days).float_timestamp) * 1000 trades = data_handler.trades_load(pair) @@ -311,8 +315,8 @@ def _download_trades_history(exchange: Exchange, def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, - timerange: TimeRange, erase: bool = False, - data_format: str = 'jsongz') -> List[str]: + timerange: TimeRange, new_pairs_days: int = 30, + erase: bool = False, data_format: str = 'jsongz') -> List[str]: """ Refresh stored trades data for backtesting and hyperopt operations. Used by freqtrade download-data subcommand. @@ -333,6 +337,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: logger.info(f'Downloading trades for pair {pair}.') _download_trades_history(exchange=exchange, pair=pair, + new_pairs_days=new_pairs_days, timerange=timerange, data_handler=data_handler) return pairs_not_available From f744df2374b8a7edb450277f640f815a0eca647f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 10:01:41 +0200 Subject: [PATCH 0267/1386] Fix bad fill message --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ffe7a7ceb..cb3dbe6c8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -246,7 +246,8 @@ class Telegram(RPCHandler): msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) + "{message_side} order for {pair} (#{trade_id}) filled " + "for {open_rate}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL: message = self._format_sell_msg(msg) From 3144185409ddeb36529b693a7bcb19df400aac20 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 11:18:28 +0300 Subject: [PATCH 0268/1386] Allow specifying "new_pairs_days" in config. --- freqtrade/commands/cli_options.py | 1 - freqtrade/constants.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 80c56ecfa..b583b47ba 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -350,7 +350,6 @@ AVAILABLE_CLI_OPTIONS = { help='Download data of new pairs for given number of days. Default: `%(default)s`.', type=check_int_positive, metavar='INT', - default=30, ), "download_trades": Arg( '--dl-trades', diff --git a/freqtrade/constants.py b/freqtrade/constants.py index aea6e1ff2..77bd2a029 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,6 +11,7 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec +NEW_PAIRS_DAYS = 30 DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -96,6 +97,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, + 'new_pairs_days': {'type': 'integer', 'default': NEW_PAIRS_DAYS}, 'timeframe': {'type': 'string'}, 'stake_currency': {'type': 'string'}, 'stake_amount': { From 7e2e196643cfa5bf6cf5aa76c55fab33139f65f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 17:13:22 +0200 Subject: [PATCH 0269/1386] improve sell_message by using sell rate --- freqtrade/rpc/telegram.py | 11 ++++++----- tests/rpc/test_rpc_telegram.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cb3dbe6c8..3eeedcd12 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -242,13 +242,14 @@ class Telegram(RPCHandler): "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): - msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' - + elif msg['type'] == RPCMessageType.BUY_FILL: message = ("\N{LARGE CIRCLE} *{exchange}:* " - "{message_side} order for {pair} (#{trade_id}) filled " + "Buy order for {pair} (#{trade_id}) filled " "for {open_rate}.".format(**msg)) - + elif msg['type'] == RPCMessageType.SELL_FILL: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Sell order for {pair} (#{trade_id}) filled " + "for {close_rate}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL: message = self._format_sell_msg(msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d72ba36ad..6a36c12a7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1372,6 +1372,35 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: telegram._rpc._fiat_converter.convert_amount = old_convamount +def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: + + default_conf['telegram']['notification_settings']['sell_fill'] = 'on' + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + telegram.send_msg({ + 'type': RPCMessageType.SELL_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/USDT', + 'gain': 'loss', + 'limit': 3.201e-05, + 'amount': 0.1, + 'order_type': 'market', + 'open_rate': 500, + 'close_rate': 550, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_ratio': -0.57405275, + 'stake_currency': 'ETH', + 'fiat_currency': 'USD', + 'sell_reason': SellType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(hours=-1), + 'close_date': arrow.utcnow(), + }) + assert msg_mock.call_args[0][0] \ + == ('\N{LARGE CIRCLE} *Binance:* Sell order for ETH/USDT (#1) filled for 550.') + + def test_send_msg_status_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) From 0d2457cd478978dfbad3d2de526be03170176c35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 19:28:39 +0200 Subject: [PATCH 0270/1386] Add lock_reason to per-pair lock --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a64d3f4cc..c3a4bc0e0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -456,7 +456,8 @@ class FreqtradeBot(LoggingMixin): lock = PairLocks.get_pair_longest_lock(pair, nowtime) if lock: self.log_once(f"Pair {pair} is still locked until " - f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}.", + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} " + f"due to {lock.reason}.", logger.info) else: self.log_once(f"Pair {pair} is still locked.", logger.info) From ccaf5764da5f9d63c77bb901a0b89541cb3ea661 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 19:41:01 +0200 Subject: [PATCH 0271/1386] Small adjustments --- docs/data-download.md | 8 +++++--- freqtrade/constants.py | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 7b09cf49c..01561c89b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -21,8 +21,9 @@ You can use a relative timerange (`--days 20`) or an absolute starting point (`- usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] - [--days INT] [--timerange TIMERANGE] - [--dl-trades] [--exchange EXCHANGE] + [--days INT] [--new-pairs-days INT] + [--timerange TIMERANGE] [--dl-trades] + [--exchange EXCHANGE] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] @@ -35,7 +36,8 @@ optional arguments: separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. - --new-pairs-days INT Download data of new pairs for given number of days. Default: `30`. + --new-pairs-days INT Download data of new pairs for given number of days. + Default: `None`. --timerange TIMERANGE Specify what timerange of data to use. --dl-trades Download trades instead of OHLCV data. The bot will diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 77bd2a029..bfd1e72f1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,7 +11,6 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec -NEW_PAIRS_DAYS = 30 DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -97,7 +96,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, - 'new_pairs_days': {'type': 'integer', 'default': NEW_PAIRS_DAYS}, + 'new_pairs_days': {'type': 'integer', 'default': 30}, 'timeframe': {'type': 'string'}, 'stake_currency': {'type': 'string'}, 'stake_amount': { From 406c1267a2cc2f396656be2ea0d96293c8335d88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 20:01:08 +0200 Subject: [PATCH 0272/1386] Remove superfluss space --- freqtrade/data/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index c9d4ef19f..af6c6a2ef 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -113,7 +113,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 if len_before != len_after: message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}" - f" - {round(pct_missing * 100, 2)} %") + f" - {round(pct_missing * 100, 2)}%") if pct_missing > 0.01: logger.info(message) else: From 4005708f85b8b1885143168e5f9cf0323d34ef09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 06:50:39 +0200 Subject: [PATCH 0273/1386] Handle edge with volumepairlist and empty pair_whitelist closes #4779 --- freqtrade/edge/edge_positioning.py | 9 +++++++-- tests/edge/test_edge.py | 23 ++++++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index d1f76c21f..334aabfab 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -81,10 +81,15 @@ class Edge: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee(symbol=expand_pairlist( - self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) + try: + self.fee = self.exchange.get_fee(symbol=expand_pairlist( + self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) + except IndexError: + self.fee = None def calculate(self, pairs: List[str]) -> bool: + if self.fee is None and pairs: + self.fee = self.exchange.get_fee(pairs[0]) heartbeat = self.edge_config.get('process_throttle_secs') diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 5142dd985..25e0da5e2 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -330,11 +330,11 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) - mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data', ) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) # Return empty - mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', return_value=[]) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) @@ -342,6 +342,23 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): assert log_has("No trades found.", caplog) +def test_edge_process_no_pairs(mocker, edge_conf, caplog): + edge_conf['exchange']['pair_whitelist'] = [] + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data') + mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) + # Return empty + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', return_value=[]) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + assert fee_mock.call_count == 0 + assert edge.fee is None + + assert not edge.calculate(['XRP/USDT']) + assert fee_mock.call_count == 1 + assert edge.fee == 0.001 + + def test_edge_init_error(mocker, edge_conf,): edge_conf['stake_amount'] = 0.5 mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) From b69a9134f59f3eed093dc0fad8c5b7191cdcb888 Mon Sep 17 00:00:00 2001 From: saeedrss Date: Fri, 23 Apr 2021 21:27:13 +0430 Subject: [PATCH 0274/1386] fixing support for HitBTC #4778 hitbtc by default send candle from beginning (not most recently) this change fixed --- freqtrade/exchange/exchange.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ed7918b36..809cdb4e1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -874,8 +874,15 @@ class Exchange: "Fetching pair %s, interval %s, since %s %s...", pair, timeframe, since_ms, s ) - - data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, + #fixing support for HitBTC #4778 + if self.name== 'HitBTC': + data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, + since=since_ms, + limit=self.ohlcv_candle_limit(timeframe), + params={"sort": "DESC"} + ) + else: + data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms, limit=self.ohlcv_candle_limit(timeframe)) From df16fbd742bd6b0f11a9391c57f8a828f9bc5d68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 19:22:41 +0200 Subject: [PATCH 0275/1386] Add "dataload complete" message to backtest + hyperopt --- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a1d4a2578..4731e6a38 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -478,6 +478,7 @@ class Backtesting: data: Dict[str, Any] = {} data, timerange = self.load_bt_data() + logger.info("Dataload complete. Calculating indicators") for strat in self.strategylist: min_date, max_date = self.backtest_one_strategy(strat, data, timerange) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d6003cf86..d1dabff36 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -379,7 +379,7 @@ class Hyperopt: logger.info(f"Using optimizer random state: {self.random_state}") self.hyperopt_table_header = -1 data, timerange = self.backtesting.load_bt_data() - + logger.info("Dataload complete. Calculating indicators") preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe From 191a31db30403fcef3dad510167c45c41599c833 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 19:36:26 +0200 Subject: [PATCH 0276/1386] NameErrors should not stop loading a different strategy --- freqtrade/resolvers/iresolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 37cfd70e6..b51795e9e 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -61,7 +61,7 @@ class IResolver: module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError, ImportError) as err: + except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: # Catch errors in case a specific module is not installed logger.warning(f"Could not import {module_path} due to '{err}'") if enum_failed: From 9dc7f776d98a0e1db3412b1baa76ed8c93055d5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 20:35:30 +0200 Subject: [PATCH 0277/1386] Improve log output when loading parameters --- freqtrade/strategy/hyper.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 16b576a73..988eae531 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -260,12 +260,15 @@ class HyperStrategyMixin(object): :param params: Dictionary with new parameter values. """ if not params: - return + logger.info(f"No params for {space} found, using default values.") + for attr_name, attr in self.enumerate_parameters(): - if attr_name in params: + if params and attr_name in params: if attr.load: attr.value = params[attr_name] logger.info(f'Strategy Parameter: {attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') + else: + logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') From 90476c4287aebacc7dc148950f420cffe8e8f038 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 07:00:33 +0200 Subject: [PATCH 0278/1386] Add "range" property to IntParameter --- freqtrade/strategy/hyper.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 988eae531..32486136d 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -5,7 +5,7 @@ This module defines a base class for auto-hyperoptable strategies. import logging from abc import ABC, abstractmethod from contextlib import suppress -from typing import Any, Iterator, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): @@ -13,6 +13,7 @@ with suppress(ImportError): from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -25,6 +26,7 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any + hyperopt: bool = False def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -121,6 +123,20 @@ class IntParameter(NumericParameter): """ return Integer(low=self.low, high=self.high, name=name, **self._space_params) + @property + def range(self): + """ + Get each value in this space as list. + Returns a List from low to high (inclusive) in Hyperopt mode. + Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid + calculating 100ds of indicators. + """ + if self.hyperopt: + # Scikit-optimize ranges are "inclusive", while python's "range" is exclusive + return range(self.low, self.high + 1) + else: + return range(self.value, self.value + 1) + class RealParameter(NumericParameter): default: float @@ -227,12 +243,11 @@ class HyperStrategyMixin(object): strategy logic. """ - def __init__(self, *args, **kwargs): + def __init__(self, config: Dict[str, Any], *args, **kwargs): """ Initialize hyperoptable strategy mixin. """ - self._load_params(getattr(self, 'buy_params', None)) - self._load_params(getattr(self, 'sell_params', None)) + self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: """ @@ -254,7 +269,14 @@ class HyperStrategyMixin(object): (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr - def _load_params(self, params: dict) -> None: + def _load_hyper_params(self, hyperopt: bool = False) -> None: + """ + Load Hyperoptable parameters + """ + self._load_params(getattr(self, 'buy_params', None), 'buy', hyperopt) + self._load_params(getattr(self, 'sell_params', None), 'sell', hyperopt) + + def _load_params(self, params: dict, space: str, hyperopt: bool = False) -> None: """ Set optimizeable parameter values. :param params: Dictionary with new parameter values. @@ -263,6 +285,7 @@ class HyperStrategyMixin(object): logger.info(f"No params for {space} found, using default values.") for attr_name, attr in self.enumerate_parameters(): + attr.hyperopt = hyperopt if params and attr_name in params: if attr.load: attr.value = params[attr_name] From 5c7f278c8a21a7744978acd2ec4f8c93abffe1f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 07:18:35 +0200 Subject: [PATCH 0279/1386] add tests for IntParameter.range --- tests/optimize/conftest.py | 2 ++ tests/optimize/test_hyperopt.py | 9 +++++++++ tests/strategy/test_interface.py | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 5c789ec1e..11b4674f3 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -6,6 +6,7 @@ import pandas as pd import pytest from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -15,6 +16,7 @@ def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ 'datadir': Path(default_conf['datadir']), + 'runmode': RunMode.HYPEROPT, 'hyperopt': 'DefaultHyperOpt', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 59bc4aefb..f725a5581 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -21,6 +21,7 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode +from freqtrade.strategy.hyper import IntParameter from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -1103,6 +1104,14 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: }) hyperopt = Hyperopt(hyperopt_conf) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) + + assert hyperopt.backtesting.strategy.buy_rsi.hyperopt is True + assert hyperopt.backtesting.strategy.buy_rsi.value == 35 + buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert isinstance(buy_rsi_range, range) + # Range from 0 - 50 (inclusive) + assert len(list(buy_rsi_range)) == 51 hyperopt.start() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 78fa368e4..347d35b19 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -588,6 +588,14 @@ def test_hyperopt_parameters(): intpar = IntParameter(low=0, high=5, default=1, space='buy') assert intpar.value == 1 assert isinstance(intpar.get_space(''), Integer) + assert isinstance(intpar.range, range) + assert len(list(intpar.range)) == 1 + # Range contains ONLY the default / value. + assert list(intpar.range) == [intpar.value] + intpar.hyperopt = True + + assert len(list(intpar.range)) == 6 + assert list(intpar.range) == [0, 1, 2, 3, 4, 5] fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy') assert isinstance(fltpar.get_space(''), Real) From d647b841f0191b4a3c2a031452b96ebe1306403d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 09:03:59 +0200 Subject: [PATCH 0280/1386] Add docs how to optimize indicator parameters --- docs/hyperopt.md | 179 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 139 insertions(+), 40 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 51905e616..bea8dc256 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -165,11 +165,22 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. - ```python + ``` bash # Have a working strategy at hand. freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 ``` +### Hyperopt execution logic + +Hyperopt will first load your data into memory and will then run `populate_indicators()` once per Pair to generate all indicators. + +Hyperopt will then spawn into different processes (number of processors, or `-j `), and run backtesting over and over again, changing the parameters that are part of the `--spaces` defined. + +For every new set of parameters, freqtrade will run first `populate_buy_trend()` followed by `populate_sell_trend()`, and then run the regular backtesting process to simulate trades. + +After backtesting, the results are passed into the [loss function](#loss-functions), which will evaluate if this result was better or worse than previous results. +Based on the loss function result, hyperopt will determine the next set of parameters to try in the next round of backtesting. + ### Configure your Guards and Triggers There are two places you need to change in your strategy file to add a new buy hyperopt for testing: @@ -188,59 +199,54 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`. Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if -ADX > 10*". - -```python -from freqtrade.strategy import IntParameter, IStrategy - -class MyAwesomeStrategy(IStrategy): - # If parameter is prefixed with `buy_` or `sell_` then specifying `space` parameter is optional - # and space is inferred from parameter name. - buy_adx_min = IntParameter(0, 100, default=10) - - def populate_buy_trend(self, dataframe: 'DataFrame', metadata: dict) -> 'DataFrame': - dataframe.loc[ - ( - (dataframe['adx'] > self.buy_adx_min.value) - ), 'buy'] = 1 - return dataframe -``` +multiple guards. #### Sell optimization Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods -* Define the parameters at the class level hyperopt shall be optimizing. +* Define the parameters at the class level hyperopt shall be optimizing, either naming them `sell_*`, or by explicitly defining `space='sell'`. * Within `populate_sell_trend()` - use defined parameter values instead of raw constants. The configuration and rules are the same than for buy signals. -```python -class MyAwesomeStrategy(IStrategy): - # There is no strict parameter naming scheme. If you do not use `buy_` or `sell_` prefixes - - # please specify to which space parameter belongs using `space` parameter. Possible values: - # 'buy' or 'sell'. - adx_max = IntParameter(0, 100, default=50, space='sell') +## Solving a Mystery - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe.loc[ - ( - (dataframe['adx'] < self.adx_max.value) - ), 'buy'] = 1 +Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys. +And you also wonder should you use RSI or ADX to help with those buy decisions. +If you decide to use RSI or ADX, which values should I use for them? + +So let's use hyperparameter optimization to solve this mystery. + +### Defining indicators to be used + +We start by calculating the indicators our strategy is going to use. + +``` python +class MyAwesomeStrategy(IStrategy): + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Generate all indicators used by the strategy + """ + dataframe['adx'] = ta.ADX(dataframe) + dataframe['rsi'] = ta.RSI(dataframe) + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) + dataframe['bb_lowerband'] = boll['lowerband'] + dataframe['bb_middleband'] = boll['middleband'] + dataframe['bb_upperband'] = boll['upperband'] return dataframe ``` -## Solving a Mystery +### Hyperoptable parameters -Let's say you are curious: should you use MACD crossings or lower Bollinger -Bands to trigger your buys. And you also wonder should you use RSI or ADX to -help with those buy decisions. If you decide to use RSI or ADX, which values -should I use for them? So let's use hyperparameter optimization to solve this -mystery. - -We will start by defining hyperoptable parameters: +We continue to define hyperoptable parameters: ```python class MyAwesomeStrategy(IStrategy): @@ -260,6 +266,8 @@ The last one we call `trigger` and use it to decide which buy trigger we want to So let's write the buy strategy using these values: ```python + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # GUARDS AND TRENDS @@ -288,7 +296,7 @@ So let's write the buy strategy using these values: ``` Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. -It will use the given historical data and make buys based on the buy signals generated with the above function. +It will use the given historical data and simulate buys based on the buy signals generated with the above function. Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). !!! Note @@ -314,6 +322,87 @@ There are four parameter types each suited for different purposes. !!! Warning Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case. +### Optimizing an indicator parameter + +Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. + +``` python +import talib.abstract as ta + +from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + stoploss = 0.5 + timeframe = '15m' + # Define the parameter spaces + buy_ema_short = IntParameter(3, 50, default=5) + buy_ema_long = IntParameter(15, 200, default=50) + + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """Generate all indicators used by the strategy""" + + # Calculate all ema_short values + for val in self.buy_ema_short.range: + dataframe[f'ema_short_{val}'] = ta.EMA(dataframe, timeperiod=val) + + # Calculate all ema_long values + for val in self.buy_ema_long.range: + dataframe[f'ema_long_{val}'] = ta.EMA(dataframe, timeperiod=val) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + conditions.append(qtpylib.crossed_above( + dataframe[f'ema_short_{self.buy_ema_short.value}'], dataframe[f'ema_long_{self.buy_ema_long.value}'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + conditions.append(qtpylib.crossed_above( + dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 + return dataframe +``` + +Breaking it down: + +Using `self.buy_ema_short.range` will return a range object containing all entries between the Parameters low and high value. +In this case (`IntParameter(3, 50, default=5)`), the loop would run for all numbers between 3 and 50 (`[3, 4, 5, ... 49, 50]`). +By using this in a loop, hyperopt will generate 48 new columns (`['buy_ema_3', 'buy_ema_4', ... , 'buy_ema_50']`). + +Hyperopt itself will then use the selected value to create the buy and sell signals + +While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters. + +!!! Note + `self.buy_ema_short.range` will act differently between hyperopt and other modes. For hyperopt, the above example may generate 48 new columns, however for all other modes (backtesting, dry/live), it will only generate the column for the selected value. You should therefore avoid using the resulting column with explicit values (values other than `self.buy_ema_short.value`). + +??? Hint "Performance tip" + By doing the calculation of all possible indicators in `populate_indicators()`, the calculation of the indicator happens only once for every parameter. + While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values). + You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. @@ -606,6 +695,16 @@ number). You can also enable position stacking in the configuration file by explicitly setting `"position_stacking"=true`. +## Out of Memory errors + +As hyperopt consumes a lot of memory (the complete data needs to be in memory once per parallel backtesting process), it's likely that you run into "out of memory" errors. +To combat these, you have multiple options: + +* reduce the amount of pairs +* reduce the timerange used (`--timerange `) +* reduce the number of parallel processes (`-j `) +* Increase the memory of your machine + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. From 7453dac668274f02dbff38a0d6dea69197b3b9f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 13:13:41 +0200 Subject: [PATCH 0281/1386] Improve doc wording --- docs/hyperopt.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index bea8dc256..b3fdc699b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -198,8 +198,7 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`. However, this guide will make this distinction to make it clear that signals should not be "sticking". Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). -Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. +Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards. #### Sell optimization @@ -266,8 +265,6 @@ The last one we call `trigger` and use it to decide which buy trigger we want to So let's write the buy strategy using these values: ```python - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # GUARDS AND TRENDS @@ -327,6 +324,9 @@ There are four parameter types each suited for different purposes. Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. ``` python +from pandas import DataFrame +from functools import reduce + import talib.abstract as ta from freqtrade.strategy import IStrategy @@ -334,7 +334,7 @@ from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParame import freqtrade.vendor.qtpylib.indicators as qtpylib class MyAwesomeStrategy(IStrategy): - stoploss = 0.5 + stoploss = -0.05 timeframe = '15m' # Define the parameter spaces buy_ema_short = IntParameter(3, 50, default=5) From 31b0e3b5e8cc672c7a94a0e708b91a96e870dc9e Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Sat, 24 Apr 2021 13:25:28 +0200 Subject: [PATCH 0282/1386] add distribution graph to example notebook --- .../templates/strategy_analysis_example.ipynb | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 491afbdd7..0bc593e2d 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -282,6 +282,28 @@ "graph.show(renderer=\"browser\")\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot average profit per trade as distribution graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.figure_factory as ff\n", + "\n", + "hist_data = [trades.profit_ratio]\n", + "group_labels = ['profit_ratio'] # name of the dataset\n", + "\n", + "fig = ff.create_distplot(hist_data, group_labels,bin_size=0.01)\n", + "fig.show()\n" + ] + }, { "cell_type": "markdown", "metadata": {}, From 185d754b8bd827d59e4eafae3edc4f6fe85077f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 13:39:20 +0200 Subject: [PATCH 0283/1386] Improve documentation to suggest config-private.json --- docs/configuration.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0ade558f1..37395c5ee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,16 @@ Per default, the bot loads the configuration from the `config.json` file, locate You can specify a different configuration file used by the bot with the `-c/--config` command line option. -In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. +Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. + +!!! Tip "Use multiple configuration files to keep secrets secret" + You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself. + + ``` bash + freqtrade trade --config user_data/config.json --config user_data/config-private.json <...> + ``` + The 2nd file should only specify what you intend to override. + If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`). If you used the [Quick start](installation.md/#quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. @@ -518,16 +527,27 @@ API Keys are usually only required for live trading (trading for real money, bot **Insert your Exchange API key (change them by fake api keys):** ```json -"exchange": { +{ + "exchange": { "name": "bittrex", "key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b", "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", - ... + //"password": "", // Optional, not needed by all exchanges) + // ... + } + //... } ``` You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange. +!!! Hint "Keep your secrets secret" + To keep your secrets secret, we recommend to use a 2nd configuration for your API keys. + Simply use the above snippet in a new configuration file (e.g. `config-private.json`) and keep your settings in this file. + You can then start the bot with `freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>` to have your keys loaded. + + **NEVER** share your private configuration file or your exchange keys with anyone! + ### Using proxy with Freqtrade To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration. From b223775385684c0698a143538ad8acb9086730ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 15:56:53 +0200 Subject: [PATCH 0284/1386] Update "output" of jupyter notebook as well --- docs/strategy_analysis_example.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 5c479aa0b..4c938500c 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -195,4 +195,18 @@ graph.show(renderer="browser") ``` +## Plot average profit per trade as distribution graph + + +```python +import plotly.figure_factory as ff + +hist_data = [trades.profit_ratio] +group_labels = ['profit_ratio'] # name of the dataset + +fig = ff.create_distplot(hist_data, group_labels,bin_size=0.01) +fig.show() + +``` + Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. From 88f26971fa6fdfc7ac05868b509d4d301fb4be8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 19:15:09 +0200 Subject: [PATCH 0285/1386] Use defaultdict for backtesting --- freqtrade/optimize/backtesting.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fc5c0fdd7..fb9826a23 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -352,7 +352,7 @@ class Backtesting: data: Dict = self._get_ohlcv_as_lists(processed) # Indexes per pair, so some pairs are allowed to have a missing start. - indexes: Dict = {} + indexes: Dict = defaultdict(int) tmp = start_date + timedelta(minutes=self.timeframe_min) open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) @@ -363,9 +363,6 @@ class Backtesting: open_trade_count_start = open_trade_count for i, pair in enumerate(data): - if pair not in indexes: - indexes[pair] = 0 - try: row = data[pair][indexes[pair]] except IndexError: From cb86c90d3ef4047c50f0fdb6392f03cc94cd86ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 19:16:54 +0200 Subject: [PATCH 0286/1386] Remove obsolete TODO's --- freqtrade/configuration/configuration.py | 2 -- freqtrade/exchange/exchange.py | 1 - 2 files changed, 3 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 86f337c1b..f6d0520c5 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -75,8 +75,6 @@ class Configuration: # Normalize config if 'internals' not in config: config['internals'] = {} - # TODO: This can be deleted along with removal of deprecated - # experimental settings if 'ask_strategy' not in config: config['ask_strategy'] = {} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ed7918b36..80b392d73 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -363,7 +363,6 @@ class Exchange: invalid_pairs = [] for pair in extended_pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs - # TODO: add a support for having coins in BTC/USDT format if self.markets and pair not in self.markets: raise OperationalException( f'Pair {pair} is not available on {self.name}. ' From e855530483d7c2ff6cbb8c813ed050565833cbbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 20:26:37 +0200 Subject: [PATCH 0287/1386] hdf5 handler should include the end-date --- freqtrade/data/history/hdf5datahandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index d116637e7..e80cfeba2 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -89,7 +89,7 @@ class HDF5DataHandler(IDataHandler): if timerange.starttype == 'date': where.append(f"date >= Timestamp({timerange.startts * 1e9})") if timerange.stoptype == 'date': - where.append(f"date < Timestamp({timerange.stopts * 1e9})") + where.append(f"date <= Timestamp({timerange.stopts * 1e9})") pairdata = pd.read_hdf(filename, key=key, mode="r", where=where) From 2eda25426f8b3bdfc6031a437456079e5ed8f8b2 Mon Sep 17 00:00:00 2001 From: wr0ngc0degen Date: Sun, 25 Apr 2021 05:47:59 +0200 Subject: [PATCH 0288/1386] fix typo in sample_strategy.py fix copy-paste issue in populate_sell_trend docstring --- freqtrade/templates/sample_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index a51b30f3f..e51feff1e 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -361,7 +361,7 @@ class SampleStrategy(IStrategy): Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame populated with indicators :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column + :return: DataFrame with sell column """ dataframe.loc[ ( From 4636b3970b29a1a82c1a1fc56f46453b7ee1f019 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 08:25:12 +0200 Subject: [PATCH 0289/1386] Fix failed test due to exchange downtime --- tests/optimize/test_hyperopt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index f725a5581..90ff8a1d0 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1094,7 +1094,9 @@ def test_print_epoch_details(capsys): assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE) -def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: +def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: + patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed del hyperopt_conf['hyperopt'] From 0fd68aee51ad010a7155aba0ff0caee8f62d57ef Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Apr 2021 10:49:09 +0300 Subject: [PATCH 0290/1386] Add IStrategy.custom_sell method which allows per-trade sell signal evaluation. --- freqtrade/strategy/interface.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 54c7f2353..b0710e833 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -285,6 +285,27 @@ class IStrategy(ABC, HyperStrategyMixin): """ return self.stoploss + def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> bool: + """ + Custom sell signal logic indicating that specified position should be sold. Returning True + from this method is equal to setting sell signal on a candle at specified time. + This method is not called when sell signal is set. + + This method should be overridden to create sell signals that depend on trade parameters. For + example you could implement a stoploss relative to candle when trade was opened, or a custom + 1:2 risk-reward ROI. + + :param pair: Pair that's currently analyzed + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: Whether trade should exit now. + """ + return False + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. @@ -535,9 +556,12 @@ class IStrategy(ABC, HyperStrategyMixin): and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal sell_signal = False - else: - sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True) + elif ask_strategy.get('use_sell_signal', True): + sell = sell or self.custom_sell(trade.pair, trade, date, current_rate, current_profit) + sell_signal = sell and not buy # TODO: return here if sell-signal should be favored over ROI + else: + sell_signal = False # Start evaluations # Sequence: From 1292e08fe47ab5ae8455c24f70612708988bd46c Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Apr 2021 11:26:03 +0300 Subject: [PATCH 0291/1386] Use strategy_safe_wrapper() when calling custom_sell(). --- freqtrade/strategy/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b0710e833..1a007da15 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -557,7 +557,8 @@ class IStrategy(ABC, HyperStrategyMixin): # sell_profit_only and profit doesn't reach the offset - ignore sell signal sell_signal = False elif ask_strategy.get('use_sell_signal', True): - sell = sell or self.custom_sell(trade.pair, trade, date, current_rate, current_profit) + sell = sell or strategy_safe_wrapper(self.custom_sell, default_retval=False)( + trade.pair, trade, date, current_rate, current_profit) sell_signal = sell and not buy # TODO: return here if sell-signal should be favored over ROI else: From a77337e4241245a8cb77f2c5cf47fe71f3bd5104 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 20 Apr 2021 10:37:45 +0300 Subject: [PATCH 0292/1386] Document IStrategy.custom_sell. --- docs/strategy-advanced.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 96c927965..758d08fca 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -69,6 +69,20 @@ class AwesomeStrategy(IStrategy): See `custom_stoploss` examples below on how to access the saved dataframe columns +## Custom sell signal + +It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. + +An example of how we can sell trades that were open longer than 1 day: + +``` python +class AwesomeStrategy(IStrategy): + def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> bool: + time_delta = datetime.datetime.utcnow() - trade.open_date_utc + return time_delta.days >= 1 +``` + ## Custom stoploss The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. From 1aad128d85dd2e9962631a1ffd8d5a3fd0dacbd1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 20 Apr 2021 11:17:00 +0300 Subject: [PATCH 0293/1386] Support returning a string from custom_sell() and have it recorded as custom sell reason. --- freqtrade/freqtradebot.py | 11 ++++--- freqtrade/optimize/backtesting.py | 2 +- freqtrade/strategy/interface.py | 50 ++++++++++++++++++++----------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ceb822472..9cce8c105 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -961,7 +961,7 @@ class FreqtradeBot(LoggingMixin): if should_sell.sell_flag: logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_sell(trade, sell_rate, should_sell.sell_type) + self.execute_sell(trade, sell_rate, should_sell.sell_type, should_sell.sell_reason) return True return False @@ -1150,12 +1150,15 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType, + custom_reason: Optional[str] = None) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order - :param sellreason: Reason the sell was triggered + :param sell_reason: Reason the sell was triggered + :param custom_reason: A custom sell reason. Provided only if + sell_reason == SellType.CUSTOM_SELL, :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' @@ -1213,7 +1216,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = sell_reason.value + trade.sell_reason = custom_reason or sell_reason.value # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fb9826a23..57ac70cc1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_type.value + trade.sell_reason = sell.sell_reason or sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1a007da15..74e92f389 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,7 +7,7 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Dict, List, NamedTuple, Optional, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -24,6 +24,7 @@ from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) +CUSTOM_SELL_MAX_LENGTH = 64 class SignalType(Enum): @@ -45,6 +46,7 @@ class SellType(Enum): SELL_SIGNAL = "sell_signal" FORCE_SELL = "force_sell" EMERGENCY_SELL = "emergency_sell" + CUSTOM_SELL = "custom_sell" NONE = "" def __str__(self): @@ -58,6 +60,7 @@ class SellCheckTuple(NamedTuple): """ sell_flag: bool sell_type: SellType + sell_reason: Optional[str] = None class IStrategy(ABC, HyperStrategyMixin): @@ -286,25 +289,28 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> bool: + current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ - Custom sell signal logic indicating that specified position should be sold. Returning True - from this method is equal to setting sell signal on a candle at specified time. - This method is not called when sell signal is set. + Custom sell signal logic indicating that specified position should be sold. Returning a + string or True from this method is equal to setting sell signal on a candle at specified + time. This method is not called when sell signal is set. This method should be overridden to create sell signals that depend on trade parameters. For example you could implement a stoploss relative to candle when trade was opened, or a custom 1:2 risk-reward ROI. + Custom sell reason max length is 64. Exceeding this limit will raise OperationalException. + :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: Whether trade should exit now. + :return: To execute sell, return a string with custom sell reason or True. Otherwise return + None or False. """ - return False + return None def informative_pairs(self) -> ListPairsWithTimeframes: """ @@ -552,17 +558,27 @@ class IStrategy(ABC, HyperStrategyMixin): and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date)) + sell_signal = SellType.NONE + custom_reason = None if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal - sell_signal = False - elif ask_strategy.get('use_sell_signal', True): - sell = sell or strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit) - sell_signal = sell and not buy + pass + elif ask_strategy.get('use_sell_signal', True) and not buy: + if sell: + sell_signal = SellType.SELL_SIGNAL + else: + custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( + trade.pair, trade, date, current_rate, current_profit) + if custom_reason: + sell_signal = SellType.CUSTOM_SELL + if isinstance(custom_reason, bool): + custom_reason = None + elif isinstance(custom_reason, str): + if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: + raise OperationalException('Custom sell reason returned ' + 'from custom_sell is too long.') # TODO: return here if sell-signal should be favored over ROI - else: - sell_signal = False # Start evaluations # Sequence: @@ -574,10 +590,10 @@ class IStrategy(ABC, HyperStrategyMixin): f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - if sell_signal: + if sell_signal != SellType.NONE: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " - f"sell_type=SellType.SELL_SIGNAL") - return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) + f"sell_type={sell_signal}, custom_reason={custom_reason}") + return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: From a90e7956958c6bcb63101f65c91d442ceaf10c83 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 21 Apr 2021 09:37:16 +0300 Subject: [PATCH 0294/1386] Warn and trim custom sell reason if it is too long. --- freqtrade/strategy/interface.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 74e92f389..435ac3ed3 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -572,12 +572,14 @@ class IStrategy(ABC, HyperStrategyMixin): trade.pair, trade, date, current_rate, current_profit) if custom_reason: sell_signal = SellType.CUSTOM_SELL - if isinstance(custom_reason, bool): - custom_reason = None - elif isinstance(custom_reason, str): + if isinstance(custom_reason, str): if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: - raise OperationalException('Custom sell reason returned ' - 'from custom_sell is too long.') + logger.warning(f'Custom sell reason returned from custom_sell is too ' + f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} ' + f'characters.') + custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH] + else: + custom_reason = None # TODO: return here if sell-signal should be favored over ROI # Start evaluations From bfad4e82adf1c6413835a8e1e27b8d49f5ae4c5d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 21 Apr 2021 10:11:54 +0300 Subject: [PATCH 0295/1386] Make execute_sell() use SellCheckTuple for sell reason. --- freqtrade/freqtradebot.py | 20 ++++++++++---------- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/strategy/interface.py | 16 +++++++++++----- tests/test_freqtradebot.py | 22 +++++++++++++--------- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9cce8c105..7888e64bc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -28,7 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State -from freqtrade.strategy.interface import IStrategy, SellType +from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -850,7 +850,8 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Selling the trade forcefully') - self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) + self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( + sell_flag=True, sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None @@ -961,7 +962,7 @@ class FreqtradeBot(LoggingMixin): if should_sell.sell_flag: logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_sell(trade, sell_rate, should_sell.sell_type, should_sell.sell_reason) + self.execute_sell(trade, sell_rate, should_sell) return True return False @@ -1150,8 +1151,7 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType, - custom_reason: Optional[str] = None) -> bool: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance @@ -1162,7 +1162,7 @@ class FreqtradeBot(LoggingMixin): :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' - if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, @@ -1179,10 +1179,10 @@ class FreqtradeBot(LoggingMixin): logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") order_type = self.strategy.order_types[sell_type] - if sell_reason == SellType.EMERGENCY_SELL: + if sell_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason == SellType.FORCE_SELL: + if sell_reason.sell_type == SellType.FORCE_SELL: # Force sells (default to the sell_type defined in the strategy, # but we allow this value to be changed) order_type = self.strategy.order_types.get("forcesell", order_type) @@ -1193,7 +1193,7 @@ class FreqtradeBot(LoggingMixin): if 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, - sell_reason=sell_reason.value): + sell_reason=sell_reason.sell_type.value): logger.info(f"User requested abortion of selling {trade.pair}") return False @@ -1216,7 +1216,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = custom_reason or sell_reason.value + trade.sell_reason = sell_reason.sell_reason # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4a8907582..957f31b63 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -24,7 +24,7 @@ from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.strategy.interface import SellType +from freqtrade.strategy.interface import SellCheckTuple, SellType logger = logging.getLogger(__name__) @@ -554,7 +554,8 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) + sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.FORCE_SELL) + self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 435ac3ed3..c73574729 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,7 +7,7 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Dict, List, NamedTuple, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -54,13 +54,18 @@ class SellType(Enum): return self.value -class SellCheckTuple(NamedTuple): +class SellCheckTuple(object): """ NamedTuple for Sell type + reason """ - sell_flag: bool + sell_flag: bool # TODO: Remove? sell_type: SellType - sell_reason: Optional[str] = None + sell_reason: Optional[str] + + def __init__(self, sell_flag: bool, sell_type: SellType, sell_reason: Optional[str] = None): + self.sell_flag = sell_flag + self.sell_type = sell_type + self.sell_reason = sell_reason or sell_type.value class IStrategy(ABC, HyperStrategyMixin): @@ -594,7 +599,8 @@ class IStrategy(ABC, HyperStrategyMixin): if sell_signal != SellType.NONE: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " - f"sell_type={sell_signal}, custom_reason={custom_reason}") + f"sell_type=SellType.{sell_signal.name}" + + (f", custom_reason={custom_reason}" if custom_reason else "")) return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ad000515e..aecff95ee 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2606,14 +2606,16 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N fetch_ticker=ticker_sell_up ) # Prevented sell ... - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 # Repatch with true freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert rpc_mock.call_count == 1 @@ -2665,7 +2667,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2722,7 +2724,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe trade.stop_loss = 0.00001099 * 0.99 freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2774,7 +2776,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade.stoploss_order_id = "abcd" freqtrade.execute_sell(trade=trade, limit=1234, - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert sellmock.call_count == 1 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2824,7 +2826,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellType.SELL_SIGNAL) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() assert trade @@ -2929,7 +2931,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee, ) freqtrade.config['order_types']['sell'] = 'market' - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert not trade.is_open assert trade.close_profit == 0.0620716 @@ -2983,8 +2986,9 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, fetch_ticker=ticker_sell_up ) + sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellType.ROI) + sell_reason=sell_reason) assert mock_insuf.call_count == 1 @@ -3226,7 +3230,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) trade.close(ticker_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) From 961b38636fa2ae7c8f4c9a97e5b90c83ff97c232 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 09:21:19 +0300 Subject: [PATCH 0296/1386] Remove explicit sell_flag parameter from SellCheckTuple. --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/rpc.py | 2 +- freqtrade/strategy/interface.py | 26 ++++++++++++++------------ tests/test_freqtradebot.py | 24 ++++++++++++------------ tests/test_integration.py | 14 +++++++------- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7888e64bc..08c69bb53 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -851,7 +851,7 @@ class FreqtradeBot(LoggingMixin): logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Selling the trade forcefully') self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( - sell_flag=True, sell_type=SellType.EMERGENCY_SELL)) + sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 957f31b63..fd97ad7d4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -554,7 +554,7 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.FORCE_SELL) + sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c73574729..bc8b3e59f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -58,15 +58,17 @@ class SellCheckTuple(object): """ NamedTuple for Sell type + reason """ - sell_flag: bool # TODO: Remove? sell_type: SellType sell_reason: Optional[str] - def __init__(self, sell_flag: bool, sell_type: SellType, sell_reason: Optional[str] = None): - self.sell_flag = sell_flag + def __init__(self, sell_type: SellType, sell_reason: Optional[str] = None): self.sell_type = sell_type self.sell_reason = sell_reason or sell_type.value + @property + def sell_flag(self): + return self.sell_type != SellType.NONE + class IStrategy(ABC, HyperStrategyMixin): """ @@ -593,25 +595,25 @@ class IStrategy(ABC, HyperStrategyMixin): # Sell-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: - logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " + logger.debug(f"{trade.pair} - Required profit reached. " f"sell_type=SellType.ROI") - return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + return SellCheckTuple(sell_type=SellType.ROI) if sell_signal != SellType.NONE: - logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " + logger.debug(f"{trade.pair} - Sell signal received. " f"sell_type=SellType.{sell_signal.name}" + (f", custom_reason={custom_reason}" if custom_reason else "")) - return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) + return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + logger.debug(f"{trade.pair} - Stoploss hit. " f"sell_type={stoplossflag.sell_type}") return stoplossflag # This one is noisy, commented out... - # logger.debug(f"{trade.pair} - No sell signal. sell_flag=False") - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + # logger.debug(f"{trade.pair} - No sell signal.") + return SellCheckTuple(sell_type=SellType.NONE) def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, @@ -675,9 +677,9 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"{trade.pair} - Trailing stop saved " f"{trade.stop_loss - trade.initial_stop_loss:.6f}") - return SellCheckTuple(sell_flag=True, sell_type=sell_type) + return SellCheckTuple(sell_type=sell_type) - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + return SellCheckTuple(sell_type=SellType.NONE) def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index aecff95ee..7a2f6a1ed 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1973,7 +1973,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, # if ROI is reached we must sell patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) - assert log_has("ETH/BTC - Required profit reached. sell_flag=True, sell_type=SellType.ROI", + assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI", caplog) @@ -2002,7 +2002,7 @@ def test_handle_trade_use_sell_signal( patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) - assert log_has("ETH/BTC - Sell signal received. sell_flag=True, sell_type=SellType.SELL_SIGNAL", + assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) @@ -2607,7 +2607,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N ) # Prevented sell ... freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -2615,7 +2615,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert rpc_mock.call_count == 1 @@ -2667,7 +2667,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2724,7 +2724,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe trade.stop_loss = 0.00001099 * 0.99 freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2776,7 +2776,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade.stoploss_order_id = "abcd" freqtrade.execute_sell(trade=trade, limit=1234, - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert sellmock.call_count == 1 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2826,7 +2826,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() assert trade @@ -2932,7 +2932,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, freqtrade.config['order_types']['sell'] = 'market' freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert not trade.is_open assert trade.close_profit == 0.0620716 @@ -2986,7 +2986,7 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, fetch_ticker=ticker_sell_up ) - sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + sell_reason = SellCheckTuple(sell_type=SellType.ROI) assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=sell_reason) assert mock_insuf.call_count == 1 @@ -3081,7 +3081,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_flag=False, sell_type=SellType.NONE)) + sell_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() @@ -3230,7 +3230,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade.close(ticker_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) diff --git a/tests/test_integration.py b/tests/test_integration.py index be0dd1137..217910961 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -51,8 +51,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) # Sell 3rd trade (not called for the first trade) should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)] + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.SELL_SIGNAL)] ) cancel_order_mock = MagicMock() mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) @@ -156,11 +156,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc _notify_sell=MagicMock(), ) should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL), - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=None, sell_type=SellType.NONE)] + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.SELL_SIGNAL), + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.NONE)] ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) From 595b8735f80df633834a4d8266694cdcb52287b8 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:15:56 +0300 Subject: [PATCH 0297/1386] Add dataframe parameter to custom_stoploss() and custom_sell() methods. --- freqtrade/freqtradebot.py | 13 +++++++------ freqtrade/optimize/backtesting.py | 7 ++++--- freqtrade/strategy/interface.py | 21 ++++++++++++--------- tests/strategy/test_default_strategy.py | 3 ++- tests/strategy/test_interface.py | 4 ++-- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 08c69bb53..f8c757189 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache +from pandas import DataFrame from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -783,10 +784,10 @@ class FreqtradeBot(LoggingMixin): config_ask_strategy = self.config.get('ask_strategy', {}) + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) @@ -813,13 +814,13 @@ class FreqtradeBot(LoggingMixin): # resulting in outdated RPC messages self._sell_rate_cache[trade.pair] = sell_rate - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -950,13 +951,13 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_sell(self, trade: Trade, sell_rate: float, + def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: """ Check and execute sell """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.now(timezone.utc), buy, sell, + dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 57ac70cc1..559938e9e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -247,9 +247,10 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade, + sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore + sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) @@ -396,7 +397,7 @@ class Backtesting: for trade in open_trades[pair]: # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(trade, row) + trade_entry = self._get_sell_trade_entry(processed[pair], trade, row) # Sell occured if trade_entry: # logger.debug(f"{pair} - Backtesting sell {trade}") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index bc8b3e59f..9a6901712 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -274,7 +274,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + current_profit: float, dataframe: DataFrame, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -296,7 +296,8 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> Optional[Union[str, bool]]: + current_profit: float, dataframe: DataFrame, + **kwargs) -> Optional[Union[str, bool]]: """ Custom sell signal logic indicating that specified position should be sold. Returning a string or True from this method is equal to setting sell signal on a candle at specified @@ -534,8 +535,8 @@ class IStrategy(ABC, HyperStrategyMixin): else: return False - def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, - sell: bool, low: float = None, high: float = None, + def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime, + buy: bool, sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell @@ -551,8 +552,9 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_min_max_rates(high or current_rate) - stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, - current_time=date, current_profit=current_profit, + stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate, + trade=trade, current_time=date, + current_profit=current_profit, force_stoploss=force_stoploss, high=high) # Set current rate to high for backtesting sell @@ -576,7 +578,7 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.SELL_SIGNAL else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit) + trade.pair, trade, date, current_rate, current_profit, dataframe) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -615,7 +617,7 @@ class IStrategy(ABC, HyperStrategyMixin): # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE) - def stop_loss_reached(self, current_rate: float, trade: Trade, + def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, high: float = None) -> SellCheckTuple: """ @@ -633,7 +635,8 @@ class IStrategy(ABC, HyperStrategyMixin): )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, - current_profit=current_profit) + current_profit=current_profit, + dataframe=dataframe) # Sanity check - error cases will return None if stop_loss_value: # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index ec7b3c33d..a8862e9c9 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -41,4 +41,5 @@ def test_default_strategy(result, fee): rate=20000, time_in_force='gtc', sell_reason='roi') is True assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), - current_rate=20_000, current_profit=0.05) == strategy.stoploss + current_rate=20_000, current_profit=0.05, dataframe=None + ) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 347d35b19..d2a09e466 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -360,7 +360,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, current_time=now, current_profit=profit, - force_stoploss=0, high=None) + force_stoploss=0, high=None, dataframe=None) assert isinstance(sl_flag, SellCheckTuple) assert sl_flag.sell_type == expected if expected == SellType.NONE: @@ -371,7 +371,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, current_time=now, current_profit=profit2, - force_stoploss=0, high=None) + force_stoploss=0, high=None, dataframe=None) assert sl_flag.sell_type == expected2 if expected2 == SellType.NONE: assert sl_flag.sell_flag is False From 004550529ea498fc584668d8b292e2fcab7d37e1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:31:53 +0300 Subject: [PATCH 0298/1386] Document dataframe parameter in custom_stoploss(). --- docs/strategy-advanced.md | 83 ++++++++++++++------------------------- 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 758d08fca..7648498d3 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -42,33 +42,6 @@ class AwesomeStrategy(IStrategy): *** -### Storing custom information using DatetimeIndex from `dataframe` - -Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. - -```python -import talib.abstract as ta -class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # using "ATR" here as example - dataframe['atr'] = ta.ATR(dataframe) - if self.dp.runmode.value in ('backtest', 'hyperopt'): - # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date') - return dataframe -``` - -!!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. - -!!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. - -See `custom_stoploss` examples below on how to access the saved dataframe columns - ## Custom sell signal It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. @@ -107,7 +80,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: Dataframe, + **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -157,7 +131,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date_utc: @@ -183,7 +158,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -209,7 +185,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: if current_profit < 0.04: return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss @@ -249,7 +226,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: @@ -266,14 +244,20 @@ class AwesomeStrategy(IStrategy): Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" -See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info` - !!! Warning - only use .iat[-1] in live mode, not in backtesting/hyperopt - otherwise you will look into the future + Only use `dataframe` values up until and including `current_time` value. Reading past + `current_time` you will look into the future, which will produce incorrect backtesting results + and throw an exception in dry/live runs. see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. +!!! Note + DataFrame is indexed by candle date. During dry/live runs `current_time` and + `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw + an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous + candle before using it as a `dataframe` index. + ``` python +from freqtrade.exchange import timeframe_to_prev_date from freqtrade.persistence import Trade from freqtrade.state import RunMode @@ -284,28 +268,19 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: result = 1 if self.custom_info and pair in self.custom_info and trade: - # using current_time directly (like below) will only work in backtesting. - # so check "runmode" to make sure that it's only used in backtesting/hyperopt - if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): - relative_sl = self.custom_info[pair].loc[current_time]['atr'] - # in live / dry-run, it'll be really the current time - else: - # but we can just use the last entry from an already analyzed dataframe instead - dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, - timeframe=self.timeframe) - # WARNING - # only use .iat[-1] in live mode, not in backtesting/hyperopt - # otherwise you will look into the future - # see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies - relative_sl = dataframe['atr'].iat[-1] - - if (relative_sl is not None): + # Using current_time directly would only work in backtesting. Live/dry runs need time to + # be rounded to previous candle to be used as dataframe index. Rounding must also be + # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. + current_time = timeframe_to_prev_date(self.timeframe, current_time) + current_row = dataframe.loc[current_time] + if 'atr' in current_row: # new stoploss relative to current_rate - new_stoploss = (current_rate-relative_sl)/current_rate + new_stoploss = (current_rate - current_row['atr']) / current_rate # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 From e58fe7a8cbbb978aa7c7a8ffcec4ebe0e3660414 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:45:34 +0300 Subject: [PATCH 0299/1386] Update custom_sell documentation. --- docs/strategy-advanced.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7648498d3..59c4a6a35 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,16 +46,31 @@ class AwesomeStrategy(IStrategy): It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. -An example of how we can sell trades that were open longer than 1 day: +An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: ``` python class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> bool: - time_delta = datetime.datetime.utcnow() - trade.open_date_utc - return time_delta.days >= 1 + current_profit: float, dataframe: Dataframe, **kwargs) -> bool: + trade_row = dataframe.loc[timeframe_to_prev_date(trade.open_date_utc)] + + # Sell when price falls below value in stoploss column of taken buy signal. + if 'stop_loss' in trade_row: + if current_rate <= trade_row['stop_loss'] < trade.open_rate: + return 'stop_loss' + + # Sell when price reaches value in take_profit column of taken buy signal. + if 'take_profit' in trade_row: + if trade.open_rate < trade_row['take_profit'] <= current_rate: + return 'take_profit' + + # Sell any positions at a loss if they are helpd for more than two days. + if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: + return 'unclog' ``` +See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. + ## Custom stoploss The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. From 98f6fce2ecf6decd360d5706d1c6ffca38643b5d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:48:24 +0300 Subject: [PATCH 0300/1386] Use correct sell reason in case of custom sell reason. --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f8c757189..6dcd920c7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1194,7 +1194,7 @@ class FreqtradeBot(LoggingMixin): if 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, - sell_reason=sell_reason.sell_type.value): + sell_reason=sell_reason.sell_reason): logger.info(f"User requested abortion of selling {trade.pair}") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 559938e9e..88957bafb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -256,7 +256,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_reason or sell.sell_type.value + trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -266,7 +266,7 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_type.value): + sell_reason=sell.sell_reason): return None trade.close(closerate, show_msg=False) From fd3afdc230edb38db7ea92091259af537e1ed4bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 10:10:09 +0200 Subject: [PATCH 0301/1386] plot-profit should use absolute values --- freqtrade/data/btanalysis.py | 6 +++--- freqtrade/plot/plotting.py | 8 ++++---- tests/data/test_btanalysis.py | 5 +++-- tests/test_plotting.py | 9 +++++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index c98477f4e..e07b0a40f 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -337,7 +337,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index - :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) + :param trades: DataFrame containing trades (requires columns close_date and profit_abs) :param col_name: Column name that will be assigned the results :param timeframe: Timeframe used during the operations :return: Returns df with one additional column, col_name, containing the cumulative profit. @@ -349,8 +349,8 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, timeframe_minutes = timeframe_to_minutes(timeframe) # Resample to timeframe to make sure trades match candles _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date' - )[['profit_ratio']].sum() - df.loc[:, col_name] = _trades_sum['profit_ratio'].cumsum() + )[['profit_abs']].sum() + df.loc[:, col_name] = _trades_sum['profit_abs'].cumsum() # Set first value to 0 df.loc[df.iloc[0].name, col_name] = 0 # FFill to get continuous diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 682c2b018..5ffe7f155 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -441,7 +441,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], - trades: pd.DataFrame, timeframe: str) -> go.Figure: + trades: pd.DataFrame, timeframe: str, stake_currency: str) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" df_comb = combine_dataframes_with_mean(data, "close") @@ -466,8 +466,8 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], subplot_titles=["AVG Close Price", "Combined Profit", "Profit per pair"]) fig['layout'].update(title="Freqtrade Profit plot") fig['layout']['yaxis1'].update(title='Price') - fig['layout']['yaxis2'].update(title='Profit') - fig['layout']['yaxis3'].update(title='Profit') + fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}') + fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}') fig['layout']['xaxis']['rangeslider'].update(visible=False) fig.add_trace(avgclose, 1, 1) @@ -581,6 +581,6 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], - trades, config.get('timeframe', '5m')) + trades, config.get('timeframe', '5m'), config.get('stake_currency')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / 'plot', auto_open=True) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index e42c13e18..6bde60926 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -1,3 +1,4 @@ +from math import isclose from pathlib import Path from unittest.mock import MagicMock @@ -246,7 +247,7 @@ def test_create_cum_profit(testdatadir): "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 - assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 + assert isclose(cum_profits.iloc[-1]['cum_profits'], 8.723007518796964e-06) def test_create_cum_profit1(testdatadir): @@ -264,7 +265,7 @@ def test_create_cum_profit1(testdatadir): "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 - assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 + assert isclose(cum_profits.iloc[-1]['cum_profits'], 8.723007518796964e-06) with pytest.raises(ValueError, match='Trade dataframe empty.'): create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'], diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 1752f9b94..a22c8c681 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -331,13 +331,13 @@ def test_generate_profit_graph(testdatadir): trades = trades[trades['pair'].isin(pairs)] - fig = generate_profit_graph(pairs, data, trades, timeframe="5m") + fig = generate_profit_graph(pairs, data, trades, timeframe="5m", stake_currency='BTC') assert isinstance(fig, go.Figure) assert fig.layout.title.text == "Freqtrade Profit plot" assert fig.layout.yaxis.title.text == "Price" - assert fig.layout.yaxis2.title.text == "Profit" - assert fig.layout.yaxis3.title.text == "Profit" + assert fig.layout.yaxis2.title.text == "Profit BTC" + assert fig.layout.yaxis3.title.text == "Profit BTC" figure = fig.layout.figure assert len(figure.data) == 5 @@ -356,7 +356,8 @@ def test_generate_profit_graph(testdatadir): with pytest.raises(OperationalException, match=r"No trades found.*"): # Pair cannot be empty - so it's an empty dataframe. - generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m") + generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m", + stake_currency='BTC') def test_start_plot_dataframe(mocker): From 7448a05f15db0b0ad5c4e023cbd2ea51d500f9ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 11:01:04 +0200 Subject: [PATCH 0302/1386] Use correct variable in pairlist_manager --- freqtrade/plot/plotting.py | 3 ++- freqtrade/plugins/pairlistmanager.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 5ffe7f155..d5a729ee1 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -581,6 +581,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], - trades, config.get('timeframe', '5m'), config.get('stake_currency')) + trades, config.get('timeframe', '5m'), + config.get('stake_currency', '')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / 'plot', auto_open=True) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 4e4135981..29c78084c 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -83,7 +83,7 @@ class PairListManager(): pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers) # Generate the pairlist with first Pairlist Handler in the chain - pairlist = self._pairlist_handlers[0].gen_pairlist(self._whitelist, tickers) + pairlist = self._pairlist_handlers[0].gen_pairlist(pairlist, tickers) # Process all Pairlist Handlers in the chain for pairlist_handler in self._pairlist_handlers: From 9c21c75cf5ef34cdd59d340e8e0b1e3f7b504ba0 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 13:12:33 +0300 Subject: [PATCH 0303/1386] Fix inaccuracy in docs. --- docs/strategy-advanced.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 59c4a6a35..f869a3c3a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -52,8 +52,8 @@ An example of how we can set stop-loss and take-profit targets in the dataframe class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, dataframe: Dataframe, **kwargs) -> bool: - trade_row = dataframe.loc[timeframe_to_prev_date(trade.open_date_utc)] - + trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze() + # Sell when price falls below value in stoploss column of taken buy signal. if 'stop_loss' in trade_row: if current_rate <= trade_row['stop_loss'] < trade.open_rate: @@ -292,7 +292,7 @@ class AwesomeStrategy(IStrategy): # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. current_time = timeframe_to_prev_date(self.timeframe, current_time) - current_row = dataframe.loc[current_time] + current_row = dataframe.loc[dataframe['date'] == current_time].squeeze() if 'atr' in current_row: # new stoploss relative to current_rate new_stoploss = (current_rate - current_row['atr']) / current_rate From bb7ef2f8044a8b8a70ae1a47445a1e84f8f18705 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 20:10:47 +0200 Subject: [PATCH 0304/1386] Cache pairlist in pairlist, not globally closes #4797 closes #4689 --- freqtrade/plugins/pairlist/IPairList.py | 3 +-- freqtrade/plugins/pairlist/StaticPairList.py | 3 +-- freqtrade/plugins/pairlist/VolumePairList.py | 22 ++++++++++++-------- freqtrade/plugins/pairlistmanager.py | 20 ++---------------- tests/plugins/test_pairlist.py | 7 ++----- 5 files changed, 19 insertions(+), 36 deletions(-) diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index c4a9c3e40..3e6252fc4 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -73,7 +73,7 @@ class IPairList(LoggingMixin, ABC): """ raise NotImplementedError() - def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: + def gen_pairlist(self, tickers: Dict) -> List[str]: """ Generate the pairlist. @@ -84,7 +84,6 @@ class IPairList(LoggingMixin, ABC): it will raise the exception if a Pairlist Handler is used at the first position in the chain. - :param cached_pairlist: Previously generated pairlist (cached) :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index 13d30fc47..d8623e13d 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -42,10 +42,9 @@ class StaticPairList(IPairList): """ return f"{self.name}" - def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: + def gen_pairlist(self, tickers: Dict) -> List[str]: """ Generate the pairlist - :param cached_pairlist: Previously generated pairlist (cached) :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index e85fb1805..8eff137b0 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -4,9 +4,10 @@ Volume PairList provider Provides dynamic pair list based on trade volumes """ import logging -from datetime import datetime from typing import Any, Dict, List +from cachetools.ttl import TTLCache + from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList @@ -33,7 +34,8 @@ class VolumePairList(IPairList): self._number_pairs = self._pairlistconfig['number_assets'] self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') self._min_value = self._pairlistconfig.get('min_value', 0) - self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) + self._refresh_period = self._pairlistconfig.get('refresh_period', 1800) + self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) if not self._exchange.exchange_has('fetchTickers'): raise OperationalException( @@ -63,17 +65,19 @@ class VolumePairList(IPairList): """ return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs." - def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: + def gen_pairlist(self, tickers: Dict) -> List[str]: """ Generate the pairlist - :param cached_pairlist: Previously generated pairlist (cached) :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ # Generate dynamic whitelist # Must always run if this pairlist is not the first in the list. - if self._last_refresh + self.refresh_period < datetime.now().timestamp(): - self._last_refresh = int(datetime.now().timestamp()) + pairlist = self._pair_cache.get('pairlist') + if pairlist: + # Item found - no refresh necessary + return pairlist + else: # Use fresh pairlist # Check if pair quote currency equals to the stake currency. @@ -82,9 +86,9 @@ class VolumePairList(IPairList): if (self._exchange.get_pair_quote_currency(k) == self._stake_currency and v[self._sort_key] is not None)] pairlist = [s['symbol'] for s in filtered_tickers] - else: - # Use the cached pairlist if it's not time yet to refresh - pairlist = cached_pairlist + + pairlist = self.filter_pairlist(pairlist, tickers) + self._pair_cache['pairlist'] = pairlist return pairlist diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 29c78084c..d1cdd2c5b 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -3,7 +3,7 @@ PairList manager class """ import logging from copy import deepcopy -from typing import Any, Dict, List +from typing import Dict, List from cachetools import TTLCache, cached @@ -79,11 +79,8 @@ class PairListManager(): if self._tickers_needed: tickers = self._get_cached_tickers() - # Adjust whitelist if filters are using tickers - pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers) - # Generate the pairlist with first Pairlist Handler in the chain - pairlist = self._pairlist_handlers[0].gen_pairlist(pairlist, tickers) + pairlist = self._pairlist_handlers[0].gen_pairlist(tickers) # Process all Pairlist Handlers in the chain for pairlist_handler in self._pairlist_handlers: @@ -95,19 +92,6 @@ class PairListManager(): self._whitelist = pairlist - def _prepare_whitelist(self, pairlist: List[str], tickers: Dict[str, Any]) -> List[str]: - """ - Prepare sanitized pairlist for Pairlist Handlers that use tickers data - remove - pairs that do not have ticker available - """ - if self._tickers_needed: - # Copy list since we're modifying this list - for p in deepcopy(pairlist): - if p not in tickers: - pairlist.remove(p) - - return pairlist - def verify_blacklist(self, pairlist: List[str], logmethod) -> List[str]: """ Verify and remove items from pairlist - returning a filtered pairlist. diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index bf225271f..57bbbdaf5 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -604,17 +604,14 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): get_tickers=tickers ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == 0 + assert len(freqtrade.pairlists._pairlist_handlers[0]._pair_cache) == 0 assert tickers.call_count == 0 freqtrade.pairlists.refresh_pairlist() assert tickers.call_count == 1 - assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh != 0 - lrf = freqtrade.pairlists._pairlist_handlers[0]._last_refresh + assert len(freqtrade.pairlists._pairlist_handlers[0]._pair_cache) == 1 freqtrade.pairlists.refresh_pairlist() assert tickers.call_count == 1 - # Time should not be updated. - assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers): From 4a5eba3db476b0fa10d846372e387441f4d7ad17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:25:53 +0000 Subject: [PATCH 0305/1386] Bump mkdocs-material from 7.1.2 to 7.1.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.2 to 7.1.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.2...7.1.3) 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 4d7082a7f..a431b1702 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.2 +mkdocs-material==7.1.3 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 14ef080d2886017d7b71fb73430e1cea750f332f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:02 +0000 Subject: [PATCH 0306/1386] Bump sqlalchemy from 1.4.9 to 1.4.11 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.9 to 1.4.11. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a89eb2383..08b1fef0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.48.22 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.9 +SQLAlchemy==1.4.11 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 09a3448fd4967c4a1ac33175698911e28af0ef44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:09 +0000 Subject: [PATCH 0307/1386] Bump pytest-mock from 3.5.1 to 3.6.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.5.1 to 3.6.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.5.1...v3.6.0) 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 5e96e2cc2..e3d6ddca8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.812 pytest==6.2.3 pytest-asyncio==0.15.0 pytest-cov==2.11.1 -pytest-mock==3.5.1 +pytest-mock==3.6.0 pytest-random-order==1.0.4 isort==5.8.0 From e5bdafd4ab71214665e34ae5ba8cb5baad42784d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:37 +0000 Subject: [PATCH 0308/1386] Bump ccxt from 1.48.22 to 1.48.76 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.48.22 to 1.48.76. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.48.22...1.48.76) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a89eb2383..b8f0ace87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.4 -ccxt==1.48.22 +ccxt==1.48.76 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From bdcf21e18728bf42f232085653ad982ff98d13c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:43 +0000 Subject: [PATCH 0309/1386] Bump pycoingecko from 1.4.1 to 2.0.0 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 1.4.1 to 2.0.0. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/1.4.1...2.0.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a89eb2383..bb5a5c577 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonschema==3.2.0 TA-Lib==0.4.19 technical==1.2.2 tabulate==0.8.9 -pycoingecko==1.4.1 +pycoingecko==2.0.0 jinja2==2.11.3 tables==3.6.1 blosc==1.10.2 From 02160d52e3fc5a29c15550aa24169417cb6f98c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:59 +0000 Subject: [PATCH 0310/1386] Bump scipy from 1.6.2 to 1.6.3 Bumps [scipy](https://github.com/scipy/scipy) from 1.6.2 to 1.6.3. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.6.2...v1.6.3) 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 9eb490f83..79ad722e2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.6.2 +scipy==1.6.3 scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 From 31a2285eacaa7ce011a98548809fbe314f4f3c79 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Mon, 26 Apr 2021 10:42:24 +0300 Subject: [PATCH 0311/1386] Fix mypy complaints. --- freqtrade/strategy/interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9a6901712..503a587ea 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -59,9 +59,9 @@ class SellCheckTuple(object): NamedTuple for Sell type + reason """ sell_type: SellType - sell_reason: Optional[str] + sell_reason: str = '' - def __init__(self, sell_type: SellType, sell_reason: Optional[str] = None): + def __init__(self, sell_type: SellType, sell_reason: str = ''): self.sell_type = sell_type self.sell_reason = sell_reason or sell_type.value @@ -568,7 +568,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_time=date)) sell_signal = SellType.NONE - custom_reason = None + custom_reason = '' if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal From 6f0a585bd0d68b75342e24bc273748b9f209d513 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 11:53:33 +0200 Subject: [PATCH 0312/1386] Fix random test failure due to ttl 0 issue --- tests/plugins/test_pairlist.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 57bbbdaf5..8b060c287 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring,C0103,protected-access +import time from unittest.mock import MagicMock, PropertyMock import pytest @@ -260,6 +261,8 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ freqtrade.pairlists.refresh_pairlist() assert whitelist == freqtrade.pairlists.whitelist + # Delay to allow 0 TTL cache to expire... + time.sleep(1) whitelist = ['FUEL/BTC', 'ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC'] tickers_dict['FUEL/BTC']['quoteVolume'] = 10000.0 freqtrade.pairlists.refresh_pairlist() From 298f54adff77bd408a9b7cd545a9333894e7ec7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 10:47:09 +0000 Subject: [PATCH 0313/1386] Bump pytest-asyncio from 0.15.0 to 0.15.1 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.15.0 to 0.15.1. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.15.0...v0.15.1) 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 e3d6ddca8..adf5a81c3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 pytest==6.2.3 -pytest-asyncio==0.15.0 +pytest-asyncio==0.15.1 pytest-cov==2.11.1 pytest-mock==3.6.0 pytest-random-order==1.0.4 From 3f84c37a79d4ef5f77656903898f31773bffb6f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 14:12:52 +0200 Subject: [PATCH 0314/1386] Fix wallet calls closes #4810 #4812 --- freqtrade/wallets.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index bbbe5ba5e..d5f979c24 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -99,12 +99,13 @@ class Wallets: balances = self._exchange.get_balances() for currency in balances: - self._wallets[currency] = Wallet( - currency, - balances[currency].get('free', None), - balances[currency].get('used', None), - balances[currency].get('total', None) - ) + if isinstance(balances[currency], dict): + self._wallets[currency] = Wallet( + currency, + balances[currency].get('free', None), + balances[currency].get('used', None), + balances[currency].get('total', None) + ) # Remove currencies no longer in get_balances output for currency in deepcopy(self._wallets): if currency not in balances: From dbf33271b5853d32d4bd952508ce33be2574948c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 19:27:22 +0200 Subject: [PATCH 0315/1386] Small doc changes --- docs/strategy-advanced.md | 24 +++++++++++-------- docs/strategy-customization.md | 5 ++-- freqtrade/freqtradebot.py | 2 -- freqtrade/strategy/interface.py | 1 + .../subtemplates/strategy_methods_advanced.j2 | 6 +++-- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f869a3c3a..f702b1448 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -49,10 +49,13 @@ It is possible to define custom sell signals. This is very useful when we need t An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: ``` python +from freqtrade.strategy import IStrategy, timeframe_to_prev_date + class AwesomeStrategy(IStrategy): - def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: Dataframe, **kwargs) -> bool: - trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze() + def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, dataframe: DataFrame, **kwargs): + trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) + trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() # Sell when price falls below value in stoploss column of taken buy signal. if 'stop_loss' in trade_row: @@ -64,12 +67,12 @@ class AwesomeStrategy(IStrategy): if trade.open_rate < trade_row['take_profit'] <= current_rate: return 'take_profit' - # Sell any positions at a loss if they are helpd for more than two days. + # Sell any positions at a loss if they are held for more than two days. if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: return 'unclog' ``` -See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. +See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. ## Custom stoploss @@ -95,7 +98,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: Dataframe, + current_rate: float, current_profit: float, dataframe: DataFrame, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). @@ -113,7 +116,7 @@ class AwesomeStrategy(IStrategy): :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New stoploss value, relative to the currentrate + :return float: New stoploss value, relative to the current rate """ return -0.04 ``` @@ -228,7 +231,6 @@ Instead of continuously trailing behind the current price, this example sets fix * Once profit is > 25% - set stoploss to 15% above open price. * Once profit is > 40% - set stoploss to 25% above open price. - ``` python from datetime import datetime from freqtrade.persistence import Trade @@ -255,6 +257,7 @@ class AwesomeStrategy(IStrategy): # return maximum stoploss value, keeping current stoploss price unchanged return 1 ``` + #### Custom stoploss using an indicator from dataframe example Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" @@ -266,7 +269,7 @@ Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. !!! Note - DataFrame is indexed by candle date. During dry/live runs `current_time` and + `dataframe` is indexed by candle date. During dry/live runs `current_time` and `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous candle before using it as a `dataframe` index. @@ -286,8 +289,9 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, dataframe: DataFrame, **kwargs) -> float: + # Default return value result = 1 - if self.custom_info and pair in self.custom_info and trade: + if trade: # Using current_time directly would only work in backtesting. Live/dry runs need time to # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 256b28990..59bfbde48 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -631,9 +631,10 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: - # once the profit has risin above 10%, keep the stoploss at 7% above the open price + # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: return stoploss_from_open(0.07, current_profit) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6dcd920c7..c2b15d23f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1158,8 +1158,6 @@ class FreqtradeBot(LoggingMixin): :param trade: Trade instance :param limit: limit rate for the sell order :param sell_reason: Reason the sell was triggered - :param custom_reason: A custom sell reason. Provided only if - sell_reason == SellType.CUSTOM_SELL, :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 503a587ea..5c3264c35 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -290,6 +290,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 53ededa19..c69b52cad 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -14,8 +14,9 @@ def bot_loop_start(self, **kwargs) -> None: use_custom_stoploss = True -def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs) -> float: +def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -31,6 +32,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', c :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ From 2061162d794fa53eb847dfbf2811282e5825146a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 19:54:47 +0200 Subject: [PATCH 0316/1386] Convert trade-opendate to python datetime --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 88957bafb..b03a459ea 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,7 @@ class Backtesting: low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: - trade.close_date = sell_row[DATE_IDX] + trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -294,7 +294,7 @@ class Backtesting: trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], - open_date=row[DATE_IDX], + open_date=row[DATE_IDX].to_pydatetime(), stake_amount=stake_amount, amount=round(stake_amount / row[OPEN_IDX], 8), fee_open=self.fee, @@ -316,7 +316,7 @@ class Backtesting: for trade in open_trades[pair]: sell_row = data[pair][-1] - trade.close_date = sell_row[DATE_IDX] + trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.sell_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) LocalTrade.close_bt_trade(trade) From 55faa6a84a6ed7a4fa65781987eb9f05d8de4152 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 20:18:03 +0200 Subject: [PATCH 0317/1386] safe_wrapper should use kwargs to call methods --- freqtrade/strategy/interface.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5c3264c35..645e70e8a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -579,7 +579,8 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.SELL_SIGNAL else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit, dataframe) + pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, + current_profit=current_profit, dataframe=dataframe) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -598,8 +599,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Sell-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: - logger.debug(f"{trade.pair} - Required profit reached. " - f"sell_type=SellType.ROI") + logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") return SellCheckTuple(sell_type=SellType.ROI) if sell_signal != SellType.NONE: @@ -610,8 +610,7 @@ class IStrategy(ABC, HyperStrategyMixin): if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. " - f"sell_type={stoplossflag.sell_type}") + logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}") return stoplossflag # This one is noisy, commented out... From cc916ab2e90458a54cdc8d4f247518f904e8fdd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 20:26:14 +0200 Subject: [PATCH 0318/1386] Add test for custom_sell --- tests/strategy/test_interface.py | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index d2a09e466..bd81bc80c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -382,6 +382,50 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.custom_stoploss = original_stopvalue +def test_custom_sell(default_conf, fee, caplog) -> None: + + default_conf.update({'strategy': 'DefaultStrategy'}) + + strategy = StrategyResolver.load_strategy(default_conf) + trade = Trade( + pair='ETH/BTC', + stake_amount=0.01, + amount=1, + open_date=arrow.utcnow().shift(hours=-1).datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + open_rate=1, + ) + + now = arrow.utcnow().datetime + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + + assert res.sell_flag is False + assert res.sell_type == SellType.NONE + + strategy.custom_sell = MagicMock(return_value=True) + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_flag is True + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_reason == 'custom_sell' + + strategy.custom_sell = MagicMock(return_value='hello world') + + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_flag is True + assert res.sell_reason == 'hello world' + + caplog.clear() + strategy.custom_sell = MagicMock(return_value='h' * 100) + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_flag is True + assert res.sell_reason == 'h' * 64 + assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog) + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) From 1465af50d78730aeacb71ef26c87ef5674531940 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Apr 2021 19:17:49 +0200 Subject: [PATCH 0319/1386] FTX usable configuration --- config_ftx.json.example | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/config_ftx.json.example b/config_ftx.json.example index 33e884976..facd54b25 100644 --- a/config_ftx.json.example +++ b/config_ftx.json.example @@ -1,7 +1,7 @@ { "max_open_trades": 3, - "stake_currency": "BTC", - "stake_amount": 0.05, + "stake_currency": "USD", + "stake_amount": 50, "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "timeframe": "5m", @@ -38,24 +38,24 @@ "rateLimit": 50 }, "pair_whitelist": [ - "ALGO/BTC", - "ATOM/BTC", - "BAT/BTC", - "BCH/BTC", - "BRD/BTC", - "EOS/BTC", - "ETH/BTC", - "IOTA/BTC", - "LINK/BTC", - "LTC/BTC", - "NEO/BTC", - "NXS/BTC", - "XMR/BTC", - "XRP/BTC", - "XTZ/BTC" + "BTC/USD", + "ETH/USD", + "BNB/USD", + "USDT/USD", + "LTC/USD", + "SRM/USD", + "SXP/USD", + "XRP/USD", + "DOGE/USD", + "1INCH/USD", + "CHZ/USD", + "MATIC/USD", + "LINK/USD", + "OXY/USD", + "SUSHI/USD" ], "pair_blacklist": [ - "BNB/BTC" + "FTT/USD" ] }, "pairlists": [ From 6eb947ae09416acac11c2c4b79f342d24986993b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Apr 2021 20:25:36 +0200 Subject: [PATCH 0320/1386] Move static Trade functions to right class --- freqtrade/persistence/models.py | 198 +++++++++++++++++--------------- tests/test_persistence.py | 33 +++++- 2 files changed, 132 insertions(+), 99 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e7fd488c7..afd51366a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -567,23 +567,6 @@ class LocalTrade(): else: return None - @staticmethod - def get_trades(trade_filter=None) -> Query: - """ - Helper function to query Trades using filters. - :param trade_filter: Optional filter to apply to trades - Can be either a Filter object, or a List of filters - e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])` - e.g. `(trade_filter=Trade.id == trade_id)` - :return: unsorted query object - """ - if trade_filter is not None: - if not isinstance(trade_filter, list): - trade_filter = [trade_filter] - return Trade.query.filter(*trade_filter) - else: - return Trade.query - @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, @@ -636,83 +619,7 @@ class LocalTrade(): """ Query trades from persistence layer """ - return Trade.get_trades(Trade.is_open.is_(True)).all() - - @staticmethod - def get_open_order_trades(): - """ - Returns all open trades - """ - return Trade.get_trades(Trade.open_order_id.isnot(None)).all() - - @staticmethod - def get_open_trades_without_assigned_fees(): - """ - Returns all open trades which don't have open fees set correctly - """ - return Trade.get_trades([Trade.fee_open_currency.is_(None), - Trade.orders.any(), - Trade.is_open.is_(True), - ]).all() - - @staticmethod - def get_sold_trades_without_assigned_fees(): - """ - Returns all closed trades which don't have fees set correctly - """ - return Trade.get_trades([Trade.fee_close_currency.is_(None), - Trade.orders.any(), - Trade.is_open.is_(False), - ]).all() - - @staticmethod - def total_open_trades_stakes() -> float: - """ - Calculates total invested amount in open trades - in stake currency - """ - if Trade.use_db: - total_open_stake_amount = Trade.query.with_entities( - func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() - else: - total_open_stake_amount = sum( - t.stake_amount for t in Trade.get_trades_proxy(is_open=True)) - return total_open_stake_amount or 0 - - @staticmethod - def get_overall_performance() -> List[Dict[str, Any]]: - """ - Returns List of dicts containing all Trades, including profit and trade count - """ - pair_rates = Trade.query.with_entities( - Trade.pair, - func.sum(Trade.close_profit).label('profit_sum'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum')) \ - .all() - return [ - { - 'pair': pair, - 'profit': rate, - 'count': count - } - for pair, rate, count in pair_rates - ] - - @staticmethod - def get_best_pair(): - """ - Get best pair with closed trade. - :returns: Tuple containing (pair, profit_sum) - """ - best_pair = Trade.query.with_entities( - Trade.pair, func.sum(Trade.close_profit).label('profit_sum') - ).filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum')).first() - return best_pair + return Trade.get_trades_proxy(is_open=True) @staticmethod def stoploss_reinitialization(desired_stoploss): @@ -810,7 +717,7 @@ class Trade(_DECL_BASE, LocalTrade): open_date: datetime = None, close_date: datetime = None, ) -> List['LocalTrade']: """ - Helper function to query Trades. + Helper function to query Trades.j Returns a List of trades, filtered on the parameters given. In live mode, converts the filter to a database query and returns all rows In Backtest mode, uses filters on Trade.trades to get the result. @@ -835,6 +742,107 @@ class Trade(_DECL_BASE, LocalTrade): close_date=close_date ) + @staticmethod + def get_trades(trade_filter=None) -> Query: + """ + Helper function to query Trades using filters. + NOTE: Not supported in Backtesting. + :param trade_filter: Optional filter to apply to trades + Can be either a Filter object, or a List of filters + e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])` + e.g. `(trade_filter=Trade.id == trade_id)` + :return: unsorted query object + """ + if not Trade.use_db: + raise NotImplementedError('`Trade.get_trades()` not supported in backtesting mode.') + if trade_filter is not None: + if not isinstance(trade_filter, list): + trade_filter = [trade_filter] + return Trade.query.filter(*trade_filter) + else: + return Trade.query + + @staticmethod + def get_open_order_trades(): + """ + Returns all open trades + NOTE: Not supported in Backtesting. + """ + return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + + @staticmethod + def get_open_trades_without_assigned_fees(): + """ + Returns all open trades which don't have open fees set correctly + NOTE: Not supported in Backtesting. + """ + return Trade.get_trades([Trade.fee_open_currency.is_(None), + Trade.orders.any(), + Trade.is_open.is_(True), + ]).all() + + @staticmethod + def get_sold_trades_without_assigned_fees(): + """ + Returns all closed trades which don't have fees set correctly + NOTE: Not supported in Backtesting. + """ + return Trade.get_trades([Trade.fee_close_currency.is_(None), + Trade.orders.any(), + Trade.is_open.is_(False), + ]).all() + + @staticmethod + def total_open_trades_stakes() -> float: + """ + Calculates total invested amount in open trades + in stake currency + """ + if Trade.use_db: + total_open_stake_amount = Trade.query.with_entities( + func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() + else: + total_open_stake_amount = sum( + t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True)) + return total_open_stake_amount or 0 + + @staticmethod + def get_overall_performance() -> List[Dict[str, Any]]: + """ + Returns List of dicts containing all Trades, including profit and trade count + NOTE: Not supported in Backtesting. + """ + pair_rates = Trade.query.with_entities( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum')) \ + .all() + return [ + { + 'pair': pair, + 'profit': rate, + 'count': count + } + for pair, rate, count in pair_rates + ] + + @staticmethod + def get_best_pair(): + """ + Get best pair with closed trade. + NOTE: Not supported in Backtesting. + :returns: Tuple containing (pair, profit_sum) + """ + best_pair = Trade.query.with_entities( + Trade.pair, func.sum(Trade.close_profit).label('profit_sum') + ).filter(Trade.is_open.is_(False)) \ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum')).first() + return best_pair + class PairLock(_DECL_BASE): """ diff --git a/tests/test_persistence.py b/tests/test_persistence.py index dad0e275e..8470e12c2 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -774,11 +774,16 @@ def test_adjust_min_max_rates(fee): @pytest.mark.usefixtures("init_persistence") -def test_get_open(fee): +@pytest.mark.parametrize('use_db', [True, False]) +def test_get_open(fee, use_db): + Trade.use_db = use_db + Trade.reset_trades() - create_mock_trades(fee) + create_mock_trades(fee, use_db) assert len(Trade.get_open_trades()) == 4 + Trade.use_db = True + @pytest.mark.usefixtures("init_persistence") def test_to_json(default_conf, fee): @@ -1083,6 +1088,13 @@ def test_get_trades_proxy(fee, use_db): Trade.use_db = True +def test_get_trades_backtest(): + Trade.use_db = False + with pytest.raises(NotImplementedError, match=r"`Trade.get_trades\(\)` not .*"): + Trade.get_trades([]) + Trade.use_db = True + + @pytest.mark.usefixtures("init_persistence") def test_get_overall_performance(fee): @@ -1216,11 +1228,24 @@ def test_Trade_object_idem(): trade = vars(Trade) localtrade = vars(LocalTrade) + excludes = ( + 'delete', + 'session', + 'query', + 'open_date', + 'get_best_pair', + 'get_overall_performance', + 'total_open_trades_stakes', + 'get_sold_trades_without_assigned_fees', + 'get_open_trades_without_assigned_fees', + 'get_open_order_trades', + 'get_trades', + ) + # Parent (LocalTrade) should have the same attributes for item in trade: # Exclude private attributes and open_date (as it's not assigned a default) - if (not item.startswith('_') - and item not in ('delete', 'session', 'query', 'open_date')): + if (not item.startswith('_') and item not in excludes): assert item in localtrade # Fails if only a column is added without corresponding parent field From 63c28b651982892e0ad3e1a26766090b7886f71a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 16:00:12 +0200 Subject: [PATCH 0321/1386] Remove obsolete get_balance method --- freqtrade/exchange/exchange.py | 11 ----------- tests/exchange/test_exchange.py | 23 ----------------------- tests/test_freqtradebot.py | 16 +--------------- 3 files changed, 1 insertion(+), 49 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 80b392d73..f61f89fcb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -666,17 +666,6 @@ class Exchange: raise OperationalException(f"stoploss is not implemented for {self.name}.") - @retrier - def get_balance(self, currency: str) -> float: - - # ccxt exception is already handled by get_balances - balances = self.get_balances() - balance = balances.get(currency) - if balance is None: - raise TemporaryError( - f'Could not get {currency} balance due to malformed exchange response: {balances}') - return balance['free'] - @retrier def get_balances(self) -> dict: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 27f4d0db9..573f41bda 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1248,29 +1248,6 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): assert "timeInForce" not in api_mock.create_order.call_args[0][5] -@pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_balance_prod(default_conf, mocker, exchange_name): - api_mock = MagicMock() - api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4, 'total': 123.4}}) - default_conf['dry_run'] = False - - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - - assert exchange.get_balance(currency='BTC') == 123.4 - - with pytest.raises(OperationalException): - api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError("Unknown error")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - - exchange.get_balance(currency='BTC') - - with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'): - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Kraken.get_balances', MagicMock(return_value={})) - exchange.get_balance(currency='BTC') - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balances_prod(default_conf, mocker, exchange_name): balance_item = { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ad000515e..2c6e86cf3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -82,10 +82,6 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None: def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) - ) conf = default_conf.copy() conf['runmode'] = RunMode.DRY_RUN conf['order_types'] = { @@ -117,10 +113,7 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: def test_order_dict_live(default_conf, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) - ) + conf = default_conf.copy() conf['runmode'] = RunMode.LIVE conf['order_types'] = { @@ -153,10 +146,6 @@ def test_order_dict_live(default_conf, mocker, caplog) -> None: def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) - ) freqtrade = FreqtradeBot(default_conf) @@ -181,7 +170,6 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2), buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee ) @@ -435,7 +423,6 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value=limit_buy_order_open), - get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, ) default_conf['max_open_trades'] = 0 @@ -523,7 +510,6 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=20), get_fee=fee, ) default_conf['stake_amount'] = 10 From 7c8a367442b81c6d0913af47d91089bd88c3534f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 20:36:06 +0200 Subject: [PATCH 0322/1386] Update docs to not promote stoploss / take-profit --- docs/strategy-advanced.md | 23 ++++++++++++----------- freqtrade/optimize/backtesting.py | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f702b1448..3ef121091 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -44,9 +44,9 @@ class AwesomeStrategy(IStrategy): ## Custom sell signal -It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. +It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. -An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: +An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than 1 day: ``` python from freqtrade.strategy import IStrategy, timeframe_to_prev_date @@ -58,21 +58,22 @@ class AwesomeStrategy(IStrategy): trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() # Sell when price falls below value in stoploss column of taken buy signal. - if 'stop_loss' in trade_row: - if current_rate <= trade_row['stop_loss'] < trade.open_rate: - return 'stop_loss' + # above 20% profit, sell when rsi < 80 + if current_profit > 0.2: + if trade_row['rsi'] < 80: + return 'rsi_below_80' - # Sell when price reaches value in take_profit column of taken buy signal. - if 'take_profit' in trade_row: - if trade.open_rate < trade_row['take_profit'] <= current_rate: - return 'take_profit' + # Between 2% and 10%, sell if EMA-long above EMA-short + if 0.02 < current_profit < 0.1: + if trade_row['emalong'] > trade_row['emashort']: + return 'ema_long_below_80' # Sell any positions at a loss if they are held for more than two days. - if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: + if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: return 'unclog' ``` -See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. +See [Custom stoploss using an indicator from dataframe example](#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. ## Custom stoploss diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b03a459ea..7b62661d3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -251,7 +251,8 @@ class Backtesting: sell_row: Tuple) -> Optional[LocalTrade]: sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore - sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], + sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], + sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: From 3285f6caa35ef555f8956eecd3468eae04c2e9c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 20:42:15 +0200 Subject: [PATCH 0323/1386] Improve wording in Note box --- docs/strategy-advanced.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 3ef121091..58e658509 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -270,10 +270,10 @@ Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. !!! Note - `dataframe` is indexed by candle date. During dry/live runs `current_time` and - `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw - an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous - candle before using it as a `dataframe` index. + `dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and + `trade.open_date_utc` will not match the candle date precisely and using them directly will throw + an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to the candle's open date + before using it to access `dataframe`. ``` python from freqtrade.exchange import timeframe_to_prev_date From 2b78ee254cab129aed9448e201e496961b3cb788 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 21:06:32 +0200 Subject: [PATCH 0324/1386] Version bump to 2021.4 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 5e2a1f88e..68bcad396 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.3' +__version__ = '2021.4' if __version__ == 'develop': From 5bc908870f124f27fe16f890b895da8af970c2f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Apr 2021 09:07:47 +0200 Subject: [PATCH 0325/1386] Fix documentation comment missalignment --- docs/strategy-advanced.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 58e658509..49b310303 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,7 +46,7 @@ class AwesomeStrategy(IStrategy): It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. -An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than 1 day: +An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: ``` python from freqtrade.strategy import IStrategy, timeframe_to_prev_date @@ -68,7 +68,7 @@ class AwesomeStrategy(IStrategy): if trade_row['emalong'] > trade_row['emashort']: return 'ema_long_below_80' - # Sell any positions at a loss if they are held for more than two days. + # Sell any positions at a loss if they are held for more than one day. if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: return 'unclog' ``` From 7cf8c5d6592deddcf40dbaf00a56ec8159c94d17 Mon Sep 17 00:00:00 2001 From: Nial McCallister <48334675+nmcc1212@users.noreply.github.com> Date: Thu, 29 Apr 2021 10:46:00 +0100 Subject: [PATCH 0326/1386] Docker Quick start grammatical error please install docker-compose should be installed does not make grammatical sense --- docs/docker_quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 9096000c1..5f48782d2 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -10,7 +10,7 @@ Start by downloading and installing Docker CE for your platform: * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -To simplify running freqtrade, please install [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). +To simplify running freqtrade, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). ## Freqtrade with docker-compose From 83708ae04596f7292a5a4e34a6a8ffd1c8908d33 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 29 Apr 2021 12:16:02 +0200 Subject: [PATCH 0327/1386] Update strategy-advanced.md Remove untrue comment probably left-over from more intricate example --- docs/strategy-advanced.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 49b310303..ccd4c2419 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -57,8 +57,7 @@ class AwesomeStrategy(IStrategy): trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() - # Sell when price falls below value in stoploss column of taken buy signal. - # above 20% profit, sell when rsi < 80 + # Above 20% profit, sell when rsi < 80 if current_profit > 0.2: if trade_row['rsi'] < 80: return 'rsi_below_80' From cf839e36f331983f74bc0fd1dd3bb6be73a61599 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 29 Apr 2021 12:49:51 +0200 Subject: [PATCH 0328/1386] Add to custom_sell() documentation - Flesh out infos about return type - give quick example at beginning to get reader in right mindset what this does and why it's useful --- docs/strategy-advanced.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 49b310303..68b3f201a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -44,7 +44,11 @@ class AwesomeStrategy(IStrategy): ## Custom sell signal -It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. +It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. + +For example you could implement a stoploss relative to candle when trade was opened, or a custom 1:2 risk-reward ROI. + +!!! Note Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set. `string` max length is 64 characters. Exceeding this limit will raise `OperationalException`. An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: From f2bd70dfc24e74d56cbc8b42c1610fd8785bc41d Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 29 Apr 2021 13:07:22 +0200 Subject: [PATCH 0329/1386] Add sentence about how it differs from custom_stoploss() --- docs/strategy-advanced.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 68b3f201a..8023a18ab 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,7 +46,9 @@ class AwesomeStrategy(IStrategy): It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. -For example you could implement a stoploss relative to candle when trade was opened, or a custom 1:2 risk-reward ROI. +For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. + +You should abstain from using custom_sell() signals in place of stoplosses though. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. !!! Note Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set. `string` max length is 64 characters. Exceeding this limit will raise `OperationalException`. From 4d1613a43265ea039b5b009335343920fe188063 Mon Sep 17 00:00:00 2001 From: Bernd Zeimetz Date: Sat, 24 Apr 2021 13:26:40 +0200 Subject: [PATCH 0330/1386] Add chunks function. Implementing a generator to split Lists into chunks. --- freqtrade/misc.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 6508363d6..2e255901e 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -6,7 +6,7 @@ import logging import re from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, Iterator, List from typing.io import IO import rapidjson @@ -202,3 +202,14 @@ def render_template_with_fallback(templatefile: str, templatefallbackfile: str, return render_template(templatefile, arguments) except TemplateNotFound: return render_template(templatefallbackfile, arguments) + + +def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]: + """ + Split lst into chunks of the size n. + :param lst: list to split into chunks + :param n: number of max elements per chunk + :return: None + """ + for chunk in range(0, len(lst), n): + yield (lst[chunk:chunk + n]) From 3be7bc509c603bc1f40619d61d12c34def88500c Mon Sep 17 00:00:00 2001 From: Bernd Zeimetz Date: Sat, 24 Apr 2021 13:28:24 +0200 Subject: [PATCH 0331/1386] Telegram: send locks as chunks of 25. Producing easily readable messages, hopefully always below the message lenght limit --- freqtrade/rpc/telegram.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3eeedcd12..97a01fc53 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -20,7 +20,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN from freqtrade.exceptions import OperationalException -from freqtrade.misc import round_coin_value +from freqtrade.misc import chunks, round_coin_value from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType @@ -750,17 +750,18 @@ class Telegram(RPCHandler): Handler for /locks. Returns the currently active locks """ - locks = self._rpc._rpc_locks() - message = tabulate([[ - lock['id'], - lock['pair'], - lock['lock_end_time'], - lock['reason']] for lock in locks['locks']], - headers=['ID', 'Pair', 'Until', 'Reason'], - tablefmt='simple') - message = f"
{escape(message)}
" - logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + rpc_locks = self._rpc._rpc_locks() + for locks in chunks(rpc_locks['locks'], 25): + message = tabulate([[ + lock['id'], + lock['pair'], + lock['lock_end_time'], + lock['reason']] for lock in locks], + headers=['ID', 'Pair', 'Until', 'Reason'], + tablefmt='simple') + message = f"
{escape(message)}
" + logger.debug(message) + self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only def _delete_locks(self, update: Update, context: CallbackContext) -> None: From cf03daa0fd60e9a2ae2769a89bbde4cfb92d62f0 Mon Sep 17 00:00:00 2001 From: Antreas Gribas Date: Fri, 30 Apr 2021 00:28:42 +0300 Subject: [PATCH 0332/1386] Fix bug in running hyperopt in windows 10 with preferred encoding in locale differrent from utf-8 --- freqtrade/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index e96e7f530..30ad96b33 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,6 +1,11 @@ """ Freqtrade bot """ __version__ = 'develop' +import locale +def getpreferredencoding(do_setlocale=True): + return "utf-8" +locale.getpreferredencoding = getpreferredencoding + if __version__ == 'develop': try: From 6763bd447e040af00bfa9457a5916f70ee108f17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Apr 2021 07:50:33 +0200 Subject: [PATCH 0333/1386] Fix link to poweredby image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 916f9cf17..6fd59b62d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ![freqtrade](docs/assets/freqtrade_poweredby.svg) +# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) From f3388ed9aa7ae903844213b0f5ca88fd43f7b91b Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Fri, 30 Apr 2021 14:30:37 +0200 Subject: [PATCH 0334/1386] fix IStrategy: abstract methods still need to pass through return value otherwise doing something like: ```py dataframe = super().populate_indicators(dataframe, ...) ``` won't work, because `dataframe` becomes `None`. This is needed if one of those methods uses dataframe.copy() instead of just working on reference. e.g. using `merge_informative` in `populate_indicator` in a nested class hierarchy --- freqtrade/strategy/interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 645e70e8a..63dcc75d9 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -161,6 +161,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ + return dataframe @abstractmethod def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -170,6 +171,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ + return dataframe @abstractmethod def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -179,6 +181,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: DataFrame with sell column """ + return dataframe def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: """ From 856b65206bf61b14662f3761c0dba0df560db36d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Apr 2021 19:42:41 +0200 Subject: [PATCH 0335/1386] Reduce log-frequency of AgeFilter closes #4840 --- freqtrade/plugins/pairlist/AgeFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 8a5379ca6..837e99f2a 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -71,7 +71,7 @@ class AgeFilter(IPairList): daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None if not self._validate_pair_loc(p, daily_candles): pairlist.remove(p) - logger.info(f"Validated {len(pairlist)} pairs.") + self.log_once(f"Validated {len(pairlist)} pairs.", logger.info) return pairlist def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: From 42a52ff6694c4595e27df7f3c49accb7ae3448c8 Mon Sep 17 00:00:00 2001 From: Boris Pruessmann Date: Sat, 1 May 2021 14:13:21 +0200 Subject: [PATCH 0336/1386] Docker support for arm64 --- Dockerfile.aarch64 | 58 +++++++++++++++++++++++++++++++++ build_helpers/install_ta-lib.sh | 5 ++- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.aarch64 diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 new file mode 100644 index 000000000..71c10d949 --- /dev/null +++ b/Dockerfile.aarch64 @@ -0,0 +1,58 @@ +FROM --platform=linux/arm64/v8 python:3.9.4-slim-buster as base + +# Setup env +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONFAULTHANDLER 1 +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" + +# Prepare environment +RUN mkdir /freqtrade \ + && apt-get update \ + && apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers + +WORKDIR /freqtrade + +# Install dependencies +FROM base as python-deps +RUN apt-get update \ + && apt-get -y install curl build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ + && apt-get clean \ + && pip install --upgrade pip + +# Install TA-lib +COPY build_helpers/* /tmp/ +RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* +ENV LD_LIBRARY_PATH /usr/local/lib + +# Install dependencies +COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ +USER ftuser +RUN pip install --user --no-cache-dir numpy \ + && pip install --user --no-cache-dir -r requirements-hyperopt.txt + +# Copy dependencies to runtime-image +FROM base as runtime-image +COPY --from=python-deps /usr/local/lib /usr/local/lib +ENV LD_LIBRARY_PATH /usr/local/lib + +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local + +USER ftuser +# Install and execute +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ + && mkdir /freqtrade/user_data/ \ + && freqtrade install-ui + +ENTRYPOINT ["freqtrade"] +# Default to trade mode +CMD [ "trade" ] diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index cb86e5f64..dd87cf105 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -8,10 +8,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ + && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \ + && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \ && ./configure --prefix=${INSTALL_LOC}/ \ - && make \ + && make -j$(nproc) \ && which sudo && sudo make install || make install \ && cd .. else echo "TA-lib already installed, skipping installation" fi +# && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ From e050ea8dfa671fd15cc6a124734ad1dfc9b3f82d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 15:33:03 +0200 Subject: [PATCH 0337/1386] Don't load parameters for other space --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 32486136d..1848730dd 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -284,7 +284,7 @@ class HyperStrategyMixin(object): if not params: logger.info(f"No params for {space} found, using default values.") - for attr_name, attr in self.enumerate_parameters(): + for attr_name, attr in self.enumerate_parameters(space): attr.hyperopt = hyperopt if params and attr_name in params: if attr.load: From e381df9098faa6c3ef998c3aa96bf31ff8336e3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 16:36:35 +0200 Subject: [PATCH 0338/1386] extract has_space to Hyperopt-Tools --- freqtrade/optimize/hyperopt.py | 44 ++++++++++++---------------- freqtrade/optimize/hyperopt_tools.py | 13 +++++++- tests/optimize/test_hyperopt.py | 6 ++-- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d1dabff36..361480329 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -114,7 +114,7 @@ class Hyperopt: self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False) - if self.has_space('sell'): + if HyperoptTools.has_space(self.config, 'sell'): # Make sure use_sell_signal is enabled if 'ask_strategy' not in self.config: self.config['ask_strategy'] = {} @@ -175,18 +175,18 @@ class Hyperopt: """ result: Dict = {} - if self.has_space('buy'): + if HyperoptTools.has_space(self.config, 'buy'): result['buy'] = {p.name: params.get(p.name) for p in self.hyperopt_space('buy')} - if self.has_space('sell'): + if HyperoptTools.has_space(self.config, 'sell'): result['sell'] = {p.name: params.get(p.name) for p in self.hyperopt_space('sell')} - if self.has_space('roi'): + if HyperoptTools.has_space(self.config, 'roi'): result['roi'] = self.custom_hyperopt.generate_roi_table(params) - if self.has_space('stoploss'): + if HyperoptTools.has_space(self.config, 'stoploss'): result['stoploss'] = {p.name: params.get(p.name) for p in self.hyperopt_space('stoploss')} - if self.has_space('trailing'): + if HyperoptTools.has_space(self.config, 'trailing'): result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) return result @@ -208,16 +208,6 @@ class Hyperopt: ) self.hyperopt_table_header = 2 - def has_space(self, space: str) -> bool: - """ - Tell if the space value is contained in the configuration - """ - # The 'trailing' space is not included in the 'default' set of spaces - if space == 'trailing': - return any(s in self.config['spaces'] for s in [space, 'all']) - else: - return any(s in self.config['spaces'] for s in [space, 'all', 'default']) - def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ Return the dimensions in the hyperoptimization space. @@ -227,23 +217,25 @@ class Hyperopt: """ spaces: List[Dimension] = [] - if space == 'buy' or (space is None and self.has_space('buy')): + if space == 'buy' or (space is None and HyperoptTools.has_space(self.config, 'buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() - if space == 'sell' or (space is None and self.has_space('sell')): + if space == 'sell' or (space is None and HyperoptTools.has_space(self.config, 'sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() - if space == 'roi' or (space is None and self.has_space('roi')): + if space == 'roi' or (space is None and HyperoptTools.has_space(self.config, 'roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() - if space == 'stoploss' or (space is None and self.has_space('stoploss')): + if space == 'stoploss' or (space is None + and HyperoptTools.has_space(self.config, 'stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() - if space == 'trailing' or (space is None and self.has_space('trailing')): + if space == 'trailing' or (space is None + and HyperoptTools.has_space(self.config, 'trailing')): logger.debug("Hyperopt has 'trailing' space") spaces += self.custom_hyperopt.trailing_space() @@ -257,22 +249,22 @@ class Hyperopt: params_dict = self._get_params_dict(raw_params) params_details = self._get_params_details(params_dict) - if self.has_space('roi'): + if HyperoptTools.has_space(self.config, 'roi'): self.backtesting.strategy.minimal_roi = ( # type: ignore self.custom_hyperopt.generate_roi_table(params_dict)) - if self.has_space('buy'): + if HyperoptTools.has_space(self.config, 'buy'): self.backtesting.strategy.advise_buy = ( # type: ignore self.custom_hyperopt.buy_strategy_generator(params_dict)) - if self.has_space('sell'): + if HyperoptTools.has_space(self.config, 'sell'): self.backtesting.strategy.advise_sell = ( # type: ignore self.custom_hyperopt.sell_strategy_generator(params_dict)) - if self.has_space('stoploss'): + if HyperoptTools.has_space(self.config, 'stoploss'): self.backtesting.strategy.stoploss = params_dict['stoploss'] - if self.has_space('trailing'): + if HyperoptTools.has_space(self.config, 'trailing'): d = self.custom_hyperopt.generate_trailing_params(params_dict) self.backtesting.strategy.trailing_stop = d['trailing_stop'] self.backtesting.strategy.trailing_stop_positive = d['trailing_stop_positive'] diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index d4c347f80..c39e0b943 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -4,7 +4,7 @@ import logging from collections import OrderedDict from pathlib import Path from pprint import pformat -from typing import Dict, List +from typing import Any, Dict, List import rapidjson import tabulate @@ -21,6 +21,17 @@ logger = logging.getLogger(__name__) class HyperoptTools(): + @staticmethod + def has_space(config: Dict[str, Any], space: str) -> bool: + """ + Tell if the space value is contained in the configuration + """ + # The 'trailing' space is not included in the 'default' set of spaces + if space == 'trailing': + return any(s in config['spaces'] for s in [space, 'all']) + else: + return any(s in config['spaces'] for s in [space, 'all', 'default']) + @staticmethod def _read_results(results_file: Path) -> List: """ diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 90ff8a1d0..d125cfca3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -503,10 +503,10 @@ def test_format_results(hyperopt): (['default', 'buy'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), ]) -def test_has_space(hyperopt, spaces, expected_results): +def test_has_space(hyperopt_conf, spaces, expected_results): for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: - hyperopt.config.update({'spaces': spaces}) - assert hyperopt.has_space(s) == expected_results[s] + hyperopt_conf.update({'spaces': spaces}) + assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s] def test_populate_indicators(hyperopt, testdatadir) -> None: From 555262b6e1f2c28ff03b27dc03452efb17029044 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 16:36:53 +0200 Subject: [PATCH 0339/1386] Only calculate additional indicators if the space is selected --- freqtrade/strategy/hyper.py | 9 ++++++--- tests/optimize/test_hyperopt.py | 2 +- tests/strategy/test_interface.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 1848730dd..2714ffb43 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -7,6 +7,8 @@ from abc import ABC, abstractmethod from contextlib import suppress from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union +from freqtrade.optimize.hyperopt_tools import HyperoptTools + with suppress(ImportError): from skopt.space import Integer, Real, Categorical @@ -26,7 +28,7 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any - hyperopt: bool = False + in_space: bool = False def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -131,7 +133,7 @@ class IntParameter(NumericParameter): Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid calculating 100ds of indicators. """ - if self.hyperopt: + if self.in_space and self.optimize: # Scikit-optimize ranges are "inclusive", while python's "range" is exclusive return range(self.low, self.high + 1) else: @@ -247,6 +249,7 @@ class HyperStrategyMixin(object): """ Initialize hyperoptable strategy mixin. """ + self.config = config self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: @@ -285,7 +288,7 @@ class HyperStrategyMixin(object): logger.info(f"No params for {space} found, using default values.") for attr_name, attr in self.enumerate_parameters(space): - attr.hyperopt = hyperopt + attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space) if params and attr_name in params: if attr.load: attr.value = params[attr_name] diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d125cfca3..5a7f0f32e 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1108,7 +1108,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.buy_rsi.hyperopt is True + assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range assert isinstance(buy_rsi_range, range) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index bd81bc80c..4ca514349 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -636,7 +636,7 @@ def test_hyperopt_parameters(): assert len(list(intpar.range)) == 1 # Range contains ONLY the default / value. assert list(intpar.range) == [intpar.value] - intpar.hyperopt = True + intpar.in_space = True assert len(list(intpar.range)) == 6 assert list(intpar.range) == [0, 1, 2, 3, 4, 5] From ca0749dfdd6d2b2291bc366f813e070f33a3560e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 16:58:14 +0200 Subject: [PATCH 0340/1386] Update strategy-advanced.md --- docs/strategy-advanced.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 8023a18ab..0580b2d39 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -50,7 +50,8 @@ For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. You should abstain from using custom_sell() signals in place of stoplosses though. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. -!!! Note Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set. `string` max length is 64 characters. Exceeding this limit will raise `OperationalException`. +!!! Note + Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: From 30da307d137e997d2d0c8590fa403e3e29a2f49f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 17:01:52 +0200 Subject: [PATCH 0341/1386] Remove encode/decode for hyperopt --- freqtrade/__init__.py | 5 ----- freqtrade/optimize/hyperopt.py | 3 +-- tests/optimize/test_hyperopt.py | 3 +-- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 30ad96b33..e96e7f530 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,11 +1,6 @@ """ Freqtrade bot """ __version__ = 'develop' -import locale -def getpreferredencoding(do_setlocale=True): - return "utf-8" -locale.getpreferredencoding = getpreferredencoding - if __version__ == 'develop': try: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d1dabff36..6f899aba6 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,7 +4,6 @@ This module contains the hyperopt logic """ -import locale import logging import random import warnings @@ -354,7 +353,7 @@ class Hyperopt: f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " f"Avg duration {results_metrics['duration']:5.1f} min." - ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') + ) def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: return Optimizer( diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 90ff8a1d0..637623e7d 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 -import locale import logging import re from datetime import datetime @@ -631,7 +630,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'Avg profit 2.31%. Median profit 2.31%. Total profit ' '0.00023300 BTC ( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). ' 'Avg duration 100.0 min.' - ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), + ), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, 'fastd-enabled': True, From e0ca3c014c254c9e34447a6bf417d80ac1faf8a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 17:12:48 +0200 Subject: [PATCH 0342/1386] Don't completely remove encode/decode --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6f899aba6..ef842faec 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -353,7 +353,7 @@ class Hyperopt: f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " f"Avg duration {results_metrics['duration']:5.1f} min." - ) + ).encode('utf-8', 'replace').decode('utf-8') def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: return Optimizer( From 0b280a59bc4413e46e4277b2a66e95d3baca3378 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 17:29:53 +0200 Subject: [PATCH 0343/1386] Support per exchange params for OHLCV endpoint --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/exchange.py | 15 +++++---------- freqtrade/exchange/hitbtc.py | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 freqtrade/exchange/hitbtc.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 889bb49c2..23ba2eb10 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -14,5 +14,6 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, timeframe_to_seconds, validate_exchange, validate_exchanges) from freqtrade.exchange.ftx import Ftx +from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kucoin import Kucoin diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 809cdb4e1..6642b946d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -59,6 +59,7 @@ class Exchange: _ft_has_default: Dict = { "stoploss_on_exchange": False, "order_time_in_force": ["gtc"], + "ohlcv_params": {}, "ohlcv_candle_limit": 500, "ohlcv_partial_candle": True, "trades_pagination": "time", # Possible are "time" or "id" @@ -874,17 +875,11 @@ class Exchange: "Fetching pair %s, interval %s, since %s %s...", pair, timeframe, since_ms, s ) - #fixing support for HitBTC #4778 - if self.name== 'HitBTC': - data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, - since=since_ms, - limit=self.ohlcv_candle_limit(timeframe), - params={"sort": "DESC"} - ) - else: - data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, + params = self._ft_has.get('ohlcv_params', {}) + data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms, - limit=self.ohlcv_candle_limit(timeframe)) + limit=self.ohlcv_candle_limit(timeframe), + params=params) # Some exchanges sort OHLCV in ASC order and others in DESC. # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py new file mode 100644 index 000000000..b9d32b56d --- /dev/null +++ b/freqtrade/exchange/hitbtc.py @@ -0,0 +1,26 @@ +""" hitbtx exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Hitbtc(Exchange): + """ + Hitbtc exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + # fetchCurrencies API point requires authentication for Hitbtc, + _ft_has: Dict = { + "ohlcv_candle_limit": 1000, + + "ohlcv_params": {"sort": "DESC"} + } From 1cb430f59b0116cbf66d71ea5f6d7909eeacea25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 17:41:40 +0200 Subject: [PATCH 0344/1386] Remove encoding specifics, gitattributes to echeckout as utf8 --- .gitattributes | 7 ++++--- freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index 00abd1d9d..bc0382636 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ -*.py eol=lf -*.sh eol=lf -*.ps1 eol=crlf +* encoding=UTF-8 +*.py eol=lf +*.sh eol=lf +*.ps1 eol=crlf diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ef842faec..6f899aba6 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -353,7 +353,7 @@ class Hyperopt: f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " f"Avg duration {results_metrics['duration']:5.1f} min." - ).encode('utf-8', 'replace').decode('utf-8') + ) def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: return Optimizer( From bdd0184f0bdd9329d9aaada8f7fbf91146fe0d41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 17:44:43 +0200 Subject: [PATCH 0345/1386] Small stylistic fixes --- freqtrade/exchange/hitbtc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py index b9d32b56d..763535263 100644 --- a/freqtrade/exchange/hitbtc.py +++ b/freqtrade/exchange/hitbtc.py @@ -1,4 +1,3 @@ -""" hitbtx exchange subclass """ import logging from typing import Dict @@ -21,6 +20,5 @@ class Hitbtc(Exchange): # fetchCurrencies API point requires authentication for Hitbtc, _ft_has: Dict = { "ohlcv_candle_limit": 1000, - "ohlcv_params": {"sort": "DESC"} } From ac2e1eb3d750ac79dfb692f7fa1c6fce4fda5536 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 08:44:16 +0200 Subject: [PATCH 0346/1386] Don't import joblib for regular strategies --- freqtrade/optimize/hyperopt_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index c39e0b943..a223a68bb 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -9,7 +9,6 @@ from typing import Any, Dict, List import rapidjson import tabulate from colorama import Fore, Style -from joblib import load from pandas import isna, json_normalize from freqtrade.exceptions import OperationalException @@ -37,6 +36,8 @@ class HyperoptTools(): """ Read hyperopt results from file """ + from joblib import load + logger.info("Reading epochs from '%s'", results_file) data = load(results_file) return data From c45204a2c4ef106bde1a37288a6d2f054de9864f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 08:59:21 +0200 Subject: [PATCH 0347/1386] Fix failing mocks --- tests/optimize/test_hyperopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 5a7f0f32e..cd405c52f 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -340,7 +340,7 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) + mock_load = mocker.patch('joblib.load', return_value=epochs) results_file = testdatadir / 'optimize' / 'ut_results.pickle' hyperopt_epochs = HyperoptTools._read_results(results_file) assert log_has(f"Reading epochs from '{results_file}'", caplog) @@ -350,7 +350,7 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) + mock_load = mocker.patch('joblib.load', return_value=epochs) mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) statmock = MagicMock() statmock.st_size = 5 @@ -364,7 +364,7 @@ def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: mock_load.assert_called_once() del epochs[0]['is_best'] - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) + mock_load = mocker.patch('joblib.load', return_value=epochs) with pytest.raises(OperationalException): HyperoptTools.load_previous_results(results_file) From b125c975c746a6de9f16883906b2892eba7d2dd2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 15:34:15 +0200 Subject: [PATCH 0348/1386] Rename strategy_comparison method --- freqtrade/optimize/optimize_reports.py | 4 ++-- tests/optimize/test_backtesting.py | 2 +- tests/optimize/test_optimize_reports.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index a80dc5d31..91ca619be 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -153,7 +153,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List return tabular_data -def generate_strategy_metrics(all_results: Dict) -> List[Dict]: +def generate_strategy_comparison(all_results: Dict) -> List[Dict]: """ Generate summary per strategy :param all_results: Dict of containing results for all strategies @@ -370,7 +370,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'csum_max': 0 }) - strategy_results = generate_strategy_metrics(all_results=all_results) + strategy_results = generate_strategy_comparison(all_results=all_results) result['strategy_comparison'] = strategy_results diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 00114be5b..39625978b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -817,7 +817,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): text_table_strategy=strattable_mock, generate_pair_metrics=MagicMock(), generate_sell_reason_stats=sell_reason_mock, - generate_strategy_metrics=strat_summary, + generate_strategy_comparison=strat_summary, generate_daily_stats=MagicMock(), ) patched_configuration_load_config_file(mocker, default_conf) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8119c732b..83a555eab 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -14,7 +14,7 @@ from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_metrics, store_backtest_stats, + generate_strategy_comparison, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -345,7 +345,7 @@ def test_text_table_strategy(default_conf): ' 43.33 | 0:20:00 | 3 | 0 | 0 |' ) - strategy_results = generate_strategy_metrics(all_results=results) + strategy_results = generate_strategy_comparison(all_results=results) assert text_table_strategy(strategy_results, 'BTC') == result_str From 9994fce5778efe937866a8fdc26dc95abe8fdfa7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 15:44:21 +0200 Subject: [PATCH 0349/1386] Extract generation of report for one strategy to it's own method --- freqtrade/optimize/optimize_reports.py | 257 +++++++++++++------------ 1 file changed, 139 insertions(+), 118 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 91ca619be..682483adb 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -235,6 +235,142 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: } +def generate_strategy_stats(btdata: Dict[str, DataFrame], + strategy: str, + content: Dict[str, Any], + min_date: Arrow, max_date: Arrow, + market_change: float + ) -> Dict[str, Any]: + """ + :param btdata: Backtest data + :param strategy: Strategy name + :param content: Backtest result data in the format: + {'results: results, 'config: config}}. + :param min_date: Backtest start date + :param max_date: Backtest end date + :param market_change: float indicating the market change + :return: Dictionary containing results per strategy and a stratgy summary. + """ + results: Dict[str, DataFrame] = content['results'] + if not isinstance(results, DataFrame): + return + config = content['config'] + max_open_trades = min(config['max_open_trades'], len(btdata.keys())) + starting_balance = config['dry_run_wallet'] + stake_currency = config['stake_currency'] + + pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + starting_balance=starting_balance, + results=results, skip_nan=False) + sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, + results=results) + left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + starting_balance=starting_balance, + results=results.loc[results['is_open']], + skip_nan=True) + daily_stats = generate_daily_stats(results) + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], + 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 + results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 + results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 + + backtest_days = (max_date - min_date).days + strat_stats = { + 'trades': results.to_dict(orient='records'), + 'locks': [lock.to_json() for lock in content['locks']], + 'best_pair': best_pair, + 'worst_pair': worst_pair, + 'results_per_pair': pair_results, + 'sell_reason_summary': sell_reason_stats, + 'left_open_trades': left_open_results, + 'total_trades': len(results), + 'total_volume': float(results['stake_amount'].sum()), + 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, + 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, + 'profit_total': results['profit_abs'].sum() / starting_balance, + 'profit_total_abs': results['profit_abs'].sum(), + 'backtest_start': min_date.datetime, + 'backtest_start_ts': min_date.int_timestamp * 1000, + 'backtest_end': max_date.datetime, + 'backtest_end_ts': max_date.int_timestamp * 1000, + 'backtest_days': backtest_days, + + 'backtest_run_start_ts': content['backtest_start_time'], + 'backtest_run_end_ts': content['backtest_end_time'], + + 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, + 'market_change': market_change, + 'pairlist': list(btdata.keys()), + 'stake_amount': config['stake_amount'], + 'stake_currency': config['stake_currency'], + 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), + 'starting_balance': starting_balance, + 'dry_run_wallet': starting_balance, + 'final_balance': content['final_balance'], + 'max_open_trades': max_open_trades, + 'max_open_trades_setting': (config['max_open_trades'] + if config['max_open_trades'] != float('inf') else -1), + 'timeframe': config['timeframe'], + 'timerange': config.get('timerange', ''), + 'enable_protections': config.get('enable_protections', False), + 'strategy_name': strategy, + # Parameters relevant for backtesting + 'stoploss': config['stoploss'], + 'trailing_stop': config.get('trailing_stop', False), + 'trailing_stop_positive': config.get('trailing_stop_positive'), + 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), + 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), + 'use_custom_stoploss': config.get('use_custom_stoploss', False), + 'minimal_roi': config['minimal_roi'], + 'use_sell_signal': config['ask_strategy']['use_sell_signal'], + 'sell_profit_only': config['ask_strategy']['sell_profit_only'], + 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], + 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], + **daily_stats, + } + + try: + max_drawdown, _, _, _, _ = calculate_max_drawdown( + results, value_col='profit_ratio') + drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( + results, value_col='profit_abs') + strat_stats.update({ + 'max_drawdown': max_drawdown, + 'max_drawdown_abs': drawdown_abs, + 'drawdown_start': drawdown_start, + 'drawdown_start_ts': drawdown_start.timestamp() * 1000, + 'drawdown_end': drawdown_end, + 'drawdown_end_ts': drawdown_end.timestamp() * 1000, + + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, + }) + + csum_min, csum_max = calculate_csum(results, starting_balance) + strat_stats.update({ + 'csum_min': csum_min, + 'csum_max': csum_max + }) + + except ValueError: + strat_stats.update({ + 'max_drawdown': 0.0, + 'max_drawdown_abs': 0.0, + 'max_drawdown_low': 0.0, + 'max_drawdown_high': 0.0, + 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), + 'drawdown_start_ts': 0, + 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), + 'drawdown_end_ts': 0, + 'csum_min': 0, + 'csum_max': 0 + }) + + return strat_stats + + def generate_backtest_stats(btdata: Dict[str, DataFrame], all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], min_date: Arrow, max_date: Arrow @@ -245,131 +381,16 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], { Strategy: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date - :return: - Dictionary containing results per strategy and a stratgy summary. + :return: Dictionary containing results per strategy and a stratgy summary. """ result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') for strategy, content in all_results.items(): - results: Dict[str, DataFrame] = content['results'] - if not isinstance(results, DataFrame): - continue - config = content['config'] - max_open_trades = min(config['max_open_trades'], len(btdata.keys())) - starting_balance = config['dry_run_wallet'] - stake_currency = config['stake_currency'] - - pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results, skip_nan=False) - sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, - results=results) - left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results.loc[results['is_open']], - skip_nan=True) - daily_stats = generate_daily_stats(results) - best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], - 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 - results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 - results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 - - backtest_days = (max_date - min_date).days - strat_stats = { - 'trades': results.to_dict(orient='records'), - 'locks': [lock.to_json() for lock in content['locks']], - 'best_pair': best_pair, - 'worst_pair': worst_pair, - 'results_per_pair': pair_results, - 'sell_reason_summary': sell_reason_stats, - 'left_open_trades': left_open_results, - 'total_trades': len(results), - 'total_volume': float(results['stake_amount'].sum()), - 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, - 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_abs'].sum() / starting_balance, - 'profit_total_abs': results['profit_abs'].sum(), - 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.int_timestamp * 1000, - 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.int_timestamp * 1000, - 'backtest_days': backtest_days, - - 'backtest_run_start_ts': content['backtest_start_time'], - 'backtest_run_end_ts': content['backtest_end_time'], - - 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, - 'market_change': market_change, - 'pairlist': list(btdata.keys()), - 'stake_amount': config['stake_amount'], - 'stake_currency': config['stake_currency'], - 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), - 'starting_balance': starting_balance, - 'dry_run_wallet': starting_balance, - 'final_balance': content['final_balance'], - 'max_open_trades': max_open_trades, - 'max_open_trades_setting': (config['max_open_trades'] - if config['max_open_trades'] != float('inf') else -1), - 'timeframe': config['timeframe'], - 'timerange': config.get('timerange', ''), - 'enable_protections': config.get('enable_protections', False), - 'strategy_name': strategy, - # Parameters relevant for backtesting - 'stoploss': config['stoploss'], - 'trailing_stop': config.get('trailing_stop', False), - 'trailing_stop_positive': config.get('trailing_stop_positive'), - 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), - 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), - 'use_custom_stoploss': config.get('use_custom_stoploss', False), - 'minimal_roi': config['minimal_roi'], - 'use_sell_signal': config['ask_strategy']['use_sell_signal'], - 'sell_profit_only': config['ask_strategy']['sell_profit_only'], - 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], - 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], - **daily_stats, - } + strat_stats = generate_strategy_stats(btdata, strategy, content, + min_date, max_date, market_change=market_change) result['strategy'][strategy] = strat_stats - try: - max_drawdown, _, _, _, _ = calculate_max_drawdown( - results, value_col='profit_ratio') - drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( - results, value_col='profit_abs') - strat_stats.update({ - 'max_drawdown': max_drawdown, - 'max_drawdown_abs': drawdown_abs, - 'drawdown_start': drawdown_start, - 'drawdown_start_ts': drawdown_start.timestamp() * 1000, - 'drawdown_end': drawdown_end, - 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - - 'max_drawdown_low': low_val, - 'max_drawdown_high': high_val, - }) - - csum_min, csum_max = calculate_csum(results, starting_balance) - strat_stats.update({ - 'csum_min': csum_min, - 'csum_max': csum_max - }) - - except ValueError: - strat_stats.update({ - 'max_drawdown': 0.0, - 'max_drawdown_abs': 0.0, - 'max_drawdown_low': 0.0, - 'max_drawdown_high': 0.0, - 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), - 'drawdown_start_ts': 0, - 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), - 'drawdown_end_ts': 0, - 'csum_min': 0, - 'csum_max': 0 - }) - strategy_results = generate_strategy_comparison(all_results=all_results) result['strategy_comparison'] = strategy_results From 545cba7fd833d7e9b400e34661ae44e60e9437f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 19:28:32 +0200 Subject: [PATCH 0350/1386] Refactor optimize_report we should not calculate non-daily statistics in the daily stats method --- freqtrade/optimize/optimize_reports.py | 42 +++++++++++++++++++------ tests/optimize/test_optimize_reports.py | 21 +++++++++++-- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 682483adb..77259df87 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -194,7 +194,37 @@ def generate_edge_table(results: dict) -> str: floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore +def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: + """ Generate overall trade statistics """ + if len(results) == 0: + return { + 'wins': 0, + 'losses': 0, + 'draws': 0, + 'holding_avg': timedelta(), + 'winner_holding_avg': timedelta(), + 'loser_holding_avg': timedelta(), + } + + winning_trades = results.loc[results['profit_ratio'] > 0] + draw_trades = results.loc[results['profit_ratio'] == 0] + losing_trades = results.loc[results['profit_ratio'] < 0] + + return { + 'wins': len(winning_trades), + 'losses': len(losing_trades), + 'draws': len(draw_trades), + 'holding_avg': (timedelta(minutes=round(results['trade_duration'].mean())) + if not results.empty else timedelta()), + 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) + if not winning_trades.empty else timedelta()), + 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) + if not losing_trades.empty else timedelta()), + } + + def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: + """ Generate daily statistics """ if len(results) == 0: return { 'backtest_best_day': 0, @@ -204,8 +234,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'winning_days': 0, 'draw_days': 0, 'losing_days': 0, - 'winner_holding_avg': timedelta(), - 'loser_holding_avg': timedelta(), } daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum() daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10) @@ -217,9 +245,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: draw_days = sum(daily_profit == 0) losing_days = sum(daily_profit < 0) - winning_trades = results.loc[results['profit_ratio'] > 0] - losing_trades = results.loc[results['profit_ratio'] < 0] - return { 'backtest_best_day': best_rel, 'backtest_worst_day': worst_rel, @@ -228,10 +253,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'winning_days': winning_days, 'draw_days': draw_days, 'losing_days': losing_days, - 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) - if not winning_trades.empty else timedelta()), - 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) - if not losing_trades.empty else timedelta()), } @@ -269,6 +290,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], results=results.loc[results['is_open']], skip_nan=True) daily_stats = generate_daily_stats(results) + trade_stats = generate_trading_stats(results) best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], 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'], @@ -289,6 +311,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'total_volume': float(results['stake_amount'].sum()), 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, + 'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, @@ -329,6 +352,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], **daily_stats, + **trade_stats } try: diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 83a555eab..b268ebadc 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -14,7 +14,7 @@ from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_comparison, store_backtest_stats, + generate_strategy_comparison, generate_trading_stats, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -226,8 +226,6 @@ def test_generate_daily_stats(testdatadir): assert res['winning_days'] == 14 assert res['draw_days'] == 4 assert res['losing_days'] == 3 - assert res['winner_holding_avg'] == timedelta(seconds=1440) - assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) # Select empty dataframe! res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) @@ -238,6 +236,23 @@ def test_generate_daily_stats(testdatadir): assert res['losing_days'] == 0 +def test_generate_trading_stats(testdatadir): + filename = testdatadir / "backtest-result_new.json" + bt_data = load_backtest_data(filename) + res = generate_trading_stats(bt_data) + assert isinstance(res, dict) + assert res['winner_holding_avg'] == timedelta(seconds=1440) + assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) + assert 'wins' in res + assert 'losses' in res + assert 'draws' in res + + # Select empty dataframe! + res = generate_trading_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) + assert res['wins'] == 0 + assert res['losses'] == 0 + + def test_text_table_sell_reason(): results = pd.DataFrame( From 6aaaad29d7edfb095ca83ab7ad3342663f74e7cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 22:33:58 +0200 Subject: [PATCH 0351/1386] Use backtesting output for hyperopt results --- freqtrade/optimize/hyperopt.py | 68 ++++++++++++++-------------- freqtrade/optimize/hyperopt_tools.py | 41 +++++++++++------ 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 361480329..468dfd873 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -8,7 +8,7 @@ import locale import logging import random import warnings -from datetime import datetime +from datetime import datetime, timezone from math import ceil from operator import itemgetter from pathlib import Path @@ -30,6 +30,8 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.optimize_reports import generate_strategy_stats +from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -79,9 +81,8 @@ class Hyperopt: self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") strategy = str(self.config['strategy']) - self.results_file = (self.config['user_data_dir'] / - 'hyperopt_results' / - f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') + self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' / + f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) @@ -246,6 +247,7 @@ class Hyperopt: Used Optimize function. Called once per epoch to optimize whatever is configured. Keep this function as optimized as possible! """ + backtest_start_time = datetime.now(timezone.utc) params_dict = self._get_params_dict(raw_params) params_details = self._get_params_details(params_dict) @@ -284,19 +286,31 @@ class Hyperopt: max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, enable_protections=self.config.get('enable_protections', False), - ) - return self._get_results_dict(backtesting_results, min_date, max_date, + backtest_end_time = datetime.now(timezone.utc) + + bt_result = { + 'results': backtesting_results, + 'config': self.backtesting.strategy.config, + 'locks': PairLocks.get_all_locks(), + 'final_balance': self.backtesting.wallets.get_total( + self.backtesting.strategy.config['stake_currency']), + 'backtest_start_time': int(backtest_start_time.timestamp()), + 'backtest_end_time': int(backtest_end_time.timestamp()), + } + return self._get_results_dict(bt_result, min_date, max_date, params_dict, params_details, processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, params_dict, params_details, processed: Dict[str, DataFrame]): - results_metrics = self._calculate_results_metrics(backtesting_results) - results_explanation = self._format_results_explanation_string(results_metrics) - trade_count = results_metrics['trade_count'] - total_profit = results_metrics['total_profit'] + strat_stats = generate_strategy_stats(processed, '', backtesting_results, + min_date, max_date, market_change=0) + results_explanation = self._format_results_explanation_string(strat_stats) + + trade_count = strat_stats['total_trades'] + total_profit = strat_stats['profit_total'] # If this evaluation contains too short amount of trades to be # interesting -- consider it as 'bad' (assigned max. loss value) @@ -304,48 +318,32 @@ class Hyperopt: # path. We do not want to optimize 'hodl' strategies. loss: float = MAX_LOSS if trade_count >= self.config['hyperopt_min_trades']: - loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count, + loss = self.calculate_loss(results=backtesting_results['results'], + trade_count=trade_count, min_date=min_date.datetime, max_date=max_date.datetime, config=self.config, processed=processed) return { 'loss': loss, 'params_dict': params_dict, 'params_details': params_details, - 'results_metrics': results_metrics, + 'results_metrics': strat_stats, 'results_explanation': results_explanation, 'total_profit': total_profit, } - def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: - wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0]) - draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0]) - losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0]) - return { - 'trade_count': len(backtesting_results.index), - 'wins': wins, - 'draws': draws, - 'losses': losses, - 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", - 'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0, - 'median_profit': backtesting_results['profit_ratio'].median() * 100.0, - 'total_profit': backtesting_results['profit_abs'].sum(), - 'profit': backtesting_results['profit_ratio'].sum() * 100.0, - 'duration': backtesting_results['trade_duration'].mean(), - } - def _format_results_explanation_string(self, results_metrics: Dict) -> str: """ Return the formatted results explanation in a string """ stake_cur = self.config['stake_currency'] - return (f"{results_metrics['trade_count']:6d} trades. " + return (f"{results_metrics['total_trades']:6d} trades. " f"{results_metrics['wins']}/{results_metrics['draws']}" f"/{results_metrics['losses']} Wins/Draws/Losses. " - f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " - f"Median profit {results_metrics['median_profit']: 6.2f}%. " - f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " - f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " - f"Avg duration {results_metrics['duration']:5.1f} min." + f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " + f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " + f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_cur} " + f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " + f"Avg duration {results_metrics['holding_avg']} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index a223a68bb..1635c0588 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -12,7 +12,7 @@ from colorama import Fore, Style from pandas import isna, json_normalize from freqtrade.exceptions import OperationalException -from freqtrade.misc import round_dict +from freqtrade.misc import round_coin_value, round_dict logger = logging.getLogger(__name__) @@ -169,11 +169,24 @@ class HyperoptTools(): # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' - trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.winsdrawslosses', - 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best']] + if 'results_metrics.total_trades' in trials: + # New mode, using backtest result for metrics + trials['results_metrics.winsdrawslosses'] = trials.apply( + lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " + f"{x['results_metrics.losses']:>4}", axis=1) + trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades', + 'results_metrics.winsdrawslosses', + 'results_metrics.profit_mean', 'results_metrics.profit_total_abs', + 'results_metrics.profit_total', 'results_metrics.holding_avg', + 'loss', 'is_initial_point', 'is_best']] + else: + # Legacy mode + trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.winsdrawslosses', + 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best']] + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', 'Total profit', 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] @@ -188,21 +201,23 @@ class HyperoptTools(): lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') ) trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}" + if not isna(x) else "--".rjust(7, ' ') ) trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') + lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') ) + stake_currency = config['stake_currency'] trials['Profit'] = trials.apply( - lambda x: '{:,.8f} {} {}'.format( - x['Total profit'], config['stake_currency'], + lambda x: '{} {}'.format( + round_coin_value(x['Total profit'], stake_currency), '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') - ).rjust(25+len(config['stake_currency'])) - if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), + ).rjust(25+len(stake_currency)) + if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), axis=1 ) trials = trials.drop(columns=['Total profit']) From 852f12534746e886f46227f1c610efe9786b5d0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Apr 2021 20:17:13 +0200 Subject: [PATCH 0352/1386] Fix tests --- tests/optimize/test_hyperopt.py | 128 +++++++++++++----------- tests/optimize/test_optimize_reports.py | 3 +- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index cd405c52f..1b1aaebad 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -5,7 +5,7 @@ import re from datetime import datetime from pathlib import Path from typing import Dict, List -from unittest.mock import MagicMock +from unittest.mock import ANY, MagicMock import pandas as pd import pytest @@ -18,10 +18,12 @@ from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.strategy.hyper import IntParameter +from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -433,18 +435,41 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: assert hasattr(hyperopt, "position_stacking") -def test_format_results(hyperopt): - # Test with BTC as stake_currency - trades = [ - ('ETH/BTC', 2, 2, 123), - ('LTC/BTC', 1, 1, 123), - ('XPR/BTC', -1, -2, -246) - ] - labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] - df = pd.DataFrame.from_records(trades, columns=labels) - results_metrics = hyperopt._calculate_results_metrics(df) +def test_hyperopt_format_results(hyperopt): + + bt_result = { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': hyperopt.config, + 'locks': [], + 'final_balance': 0.02, + 'backtest_start_time': 1619718665, + 'backtest_end_time': 1619718665, + } + results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result, + Arrow(2017, 11, 14, 19, 32, 00), + Arrow(2017, 12, 14, 19, 32, 00), market_change=0) + results_explanation = hyperopt._format_results_explanation_string(results_metrics) - total_profit = results_metrics['total_profit'] + total_profit = results_metrics['profit_total_abs'] results = { 'loss': 0.0, @@ -458,21 +483,9 @@ def test_format_results(hyperopt): } result = HyperoptTools._format_explanation_string(results, 1) - assert result.find(' 66.67%') - assert result.find('Total profit 1.00000000 BTC') - assert result.find('2.0000Σ %') - - # Test with EUR as stake_currency - trades = [ - ('ETH/EUR', 2, 2, 123), - ('LTC/EUR', 1, 1, 123), - ('XPR/EUR', -1, -2, -246) - ] - df = pd.DataFrame.from_records(trades, columns=labels) - results_metrics = hyperopt._calculate_results_metrics(df) - results['total_profit'] = results_metrics['total_profit'] - result = HyperoptTools._format_explanation_string(results, 1) - assert result.find('Total profit 1.00000000 EUR') + assert ' 0.71%' in result + assert 'Total profit 0.00003100 BTC' in result + assert '0:50:00 min' in result @pytest.mark.parametrize("spaces, expected_results", [ @@ -577,22 +590,32 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'hyperopt_min_trades': 1, }) - trades = [ - ('TRX/BTC', 0.023117, 0.000233, 100) - ] - labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] - backtest_result = pd.DataFrame.from_records(trades, columns=labels) + backtest_result = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }) - mocker.patch( - 'freqtrade.optimize.hyperopt.Backtesting.backtest', - MagicMock(return_value=backtest_result) - ) - mocker.patch( - 'freqtrade.optimize.hyperopt.get_timerange', - MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) - ) + mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result) + mocker.patch('freqtrade.optimize.hyperopt.get_timerange', + return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) patch_exchange(mocker) - mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) optimizer_param = { 'adx-value': 0, @@ -626,11 +649,11 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_only_offset_is_reached': False, } response_expected = { - 'loss': 1.9840569076926293, - 'results_explanation': (' 1 trades. 1/0/0 Wins/Draws/Losses. ' - 'Avg profit 2.31%. Median profit 2.31%. Total profit ' - '0.00023300 BTC ( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). ' - 'Avg duration 100.0 min.' + 'loss': 1.9147239021396234, + 'results_explanation': (' 4 trades. 4/0/0 Wins/Draws/Losses. ' + 'Avg profit 0.77%. Median profit 0.71%. Total profit ' + '0.00003100 BTC ( 0.00\N{GREEK CAPITAL LETTER SIGMA}%). ' + 'Avg duration 0:50:00 min.' ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, @@ -660,17 +683,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_stop_positive': 0.02, 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, - 'results_metrics': {'avg_profit': 2.3117, - 'draws': 0, - 'duration': 100.0, - 'losses': 0, - 'winsdrawslosses': ' 1 0 0', - 'median_profit': 2.3117, - 'profit': 2.3117, - 'total_profit': 0.000233, - 'trade_count': 1, - 'wins': 1}, - 'total_profit': 0.00023300 + 'results_metrics': ANY, + 'total_profit': 3.1e-08 } hyperopt = Hyperopt(hyperopt_conf) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index b268ebadc..fb4624ca3 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -14,7 +14,8 @@ from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_comparison, generate_trading_stats, store_backtest_stats, + generate_strategy_comparison, + generate_trading_stats, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver From e2e1d34828faa71936c61cf729535e2071918fdc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Apr 2021 20:20:26 +0200 Subject: [PATCH 0353/1386] Extract stake_currency param from hyperopt-explanationstring --- freqtrade/optimize/hyperopt.py | 8 ++++---- tests/optimize/test_hyperopt.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 468dfd873..9b3a22236 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -307,7 +307,8 @@ class Hyperopt: strat_stats = generate_strategy_stats(processed, '', backtesting_results, min_date, max_date, market_change=0) - results_explanation = self._format_results_explanation_string(strat_stats) + results_explanation = self._format_results_explanation_string( + strat_stats, self.config['stake_currency']) trade_count = strat_stats['total_trades'] total_profit = strat_stats['profit_total'] @@ -331,17 +332,16 @@ class Hyperopt: 'total_profit': total_profit, } - def _format_results_explanation_string(self, results_metrics: Dict) -> str: + def _format_results_explanation_string(self, results_metrics: Dict, stake_currency: str) -> str: """ Return the formatted results explanation in a string """ - stake_cur = self.config['stake_currency'] return (f"{results_metrics['total_trades']:6d} trades. " f"{results_metrics['wins']}/{results_metrics['draws']}" f"/{results_metrics['losses']} Wins/Draws/Losses. " f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " - f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_cur} " + f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " f"Avg duration {results_metrics['holding_avg']} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 1b1aaebad..a80f3dced 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -468,7 +468,7 @@ def test_hyperopt_format_results(hyperopt): Arrow(2017, 11, 14, 19, 32, 00), Arrow(2017, 12, 14, 19, 32, 00), market_change=0) - results_explanation = hyperopt._format_results_explanation_string(results_metrics) + results_explanation = hyperopt._format_results_explanation_string(results_metrics, 'BTC') total_profit = results_metrics['profit_total_abs'] results = { From f2e182002d9990f344faf537f30bee9ec9078914 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Apr 2021 07:31:57 +0200 Subject: [PATCH 0354/1386] Simplify calling backtesting by returning the proper result --- freqtrade/optimize/backtesting.py | 20 +++-- freqtrade/optimize/hyperopt.py | 16 ++-- freqtrade/optimize/optimize_reports.py | 2 +- tests/optimize/test_backtesting.py | 111 +++++++++++++++---------- 4 files changed, 83 insertions(+), 66 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7b62661d3..54e7b806f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -330,7 +330,7 @@ class Backtesting: def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, - enable_protections: bool = False) -> DataFrame: + enable_protections: bool = False) -> Dict[str, Any]: """ Implement backtesting functionality @@ -417,7 +417,13 @@ class Backtesting: trades += self.handle_left_open(open_trades, data=data) self.wallets.update() - return trade_list_to_dataframe(trades) + results = trade_list_to_dataframe(trades) + return { + 'results': results, + 'config': self.strategy.config, + 'locks': PairLocks.get_all_locks(), + 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), + } def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) @@ -457,14 +463,12 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - self.all_results[self.strategy.get_strategy_name()] = { - 'results': results, - 'config': self.strategy.config, - 'locks': PairLocks.get_all_locks(), - 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), + results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), - } + }) + self.all_results[self.strategy.get_strategy_name()] = results + return min_date, max_date def start(self) -> None: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9b3a22236..229b35dd5 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -31,7 +31,6 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats -from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -279,7 +278,7 @@ class Hyperopt: min_date, max_date = get_timerange(processed) - backtesting_results = self.backtesting.backtest( + bt_results = self.backtesting.backtest( processed=processed, start_date=min_date.datetime, end_date=max_date.datetime, @@ -288,17 +287,12 @@ class Hyperopt: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - - bt_result = { - 'results': backtesting_results, - 'config': self.backtesting.strategy.config, - 'locks': PairLocks.get_all_locks(), - 'final_balance': self.backtesting.wallets.get_total( - self.backtesting.strategy.config['stake_currency']), + bt_results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), - } - return self._get_results_dict(bt_result, min_date, max_date, + }) + + return self._get_results_dict(bt_results, min_date, max_date, params_dict, params_details, processed=processed) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 77259df87..ef5426c9a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -274,7 +274,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], """ results: Dict[str, DataFrame] = content['results'] if not isinstance(results, DataFrame): - return + return {} config = content['config'] max_open_trades = min(config['max_open_trades'], len(btdata.keys())) starting_balance = config['dry_run_wallet'] diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 39625978b..e09f02c66 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -514,13 +514,14 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: timerange=timerange) processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) - results = backtesting.backtest( + result = backtesting.backtest( processed=processed, start_date=min_date, end_date=max_date, max_open_trades=10, position_stacking=False, ) + results = result['results'] assert not results.empty assert len(results) == 2 @@ -583,8 +584,8 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None max_open_trades=1, position_stacking=False, ) - assert not results.empty - assert len(results) == 1 + assert not results['results'].empty + assert len(results['results']) == 1 def test_processed(default_conf, mocker, testdatadir) -> None: @@ -623,7 +624,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results for [contour, numres] in tests: - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == numres + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres @pytest.mark.parametrize('protections,contour,expected', [ @@ -648,7 +649,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == expected + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): @@ -662,8 +663,8 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(**backtest_conf) - assert results.empty + result = backtesting.backtest(**backtest_conf) + assert result['results'].empty def test_backtest_only_sell(mocker, default_conf, testdatadir): @@ -677,8 +678,8 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(**backtest_conf) - assert results.empty + result = backtesting.backtest(**backtest_conf) + assert result['results'].empty def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): @@ -690,10 +691,11 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override - results = backtesting.backtest(**backtest_conf) + result = backtesting.backtest(**backtest_conf) # 200 candles in backtest data # won't buy on first (shifted by 1) # 100 buys signals + results = result['results'] assert len(results) == 100 # One trade was force-closed at the end assert len(results.loc[results['is_open']]) == 0 @@ -745,9 +747,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) results = backtesting.backtest(**backtest_conf) # Make sure we have parallel trades - assert len(evaluate_result_multi(results, '5m', 2)) > 0 + assert len(evaluate_result_multi(results['results'], '5m', 2)) > 0 # make sure we don't have trades with more than configured max_open_trades - assert len(evaluate_result_multi(results, '5m', 3)) == 0 + assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 backtest_conf = { 'processed': processed, @@ -757,7 +759,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) 'position_stacking': False, } results = backtesting.backtest(**backtest_conf) - assert len(evaluate_result_multi(results, '5m', 1)) == 0 + assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0 def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): @@ -803,7 +805,12 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) - backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS)) + backtestmock = MagicMock(return_value={ + 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) @@ -867,39 +874,51 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): patch_exchange(mocker) + result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], + 'profit_ratio': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', ], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', ], utc=True), + 'trade_duration': [235, 40], + 'is_open': [False, False], + 'stake_amount': [0.01, 0.01], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.104969, 0.103541], + 'sell_reason': [SellType.ROI, SellType.ROI] + }) + result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], + 'profit_ratio': [0.03, 0.01, 0.1], + 'profit_abs': [0.01, 0.02, 0.2], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', + '2018-01-30 05:30:00'], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', + '2018-01-30 08:30:00'], utc=True), + 'trade_duration': [47, 40, 20], + 'is_open': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], + 'open_rate': [0.104445, 0.10302485, 0.122541], + 'close_rate': [0.104969, 0.103541, 0.123541], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + }) backtestmock = MagicMock(side_effect=[ - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], - 'profit_ratio': [0.0, 0.0], - 'profit_abs': [0.0, 0.0], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', ], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', ], utc=True), - 'trade_duration': [235, 40], - 'is_open': [False, False], - 'stake_amount': [0.01, 0.01], - 'open_rate': [0.104445, 0.10302485], - 'close_rate': [0.104969, 0.103541], - 'sell_reason': [SellType.ROI, SellType.ROI] - }), - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], - 'profit_ratio': [0.03, 0.01, 0.1], - 'profit_abs': [0.01, 0.02, 0.2], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', - '2018-01-30 05:30:00'], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', - '2018-01-30 08:30:00'], utc=True), - 'trade_duration': [47, 40, 20], - 'is_open': [False, False, False], - 'stake_amount': [0.01, 0.01, 0.01], - 'open_rate': [0.104445, 0.10302485, 0.122541], - 'close_rate': [0.104969, 0.103541, 0.123541], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - }), + { + 'results': result1, + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + }, + { + 'results': result2, + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + } ]) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) From 4c00d4496de63939443f5787625e5dfa6632bcb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Apr 2021 07:40:07 +0200 Subject: [PATCH 0355/1386] Update tests to reflect new backtest returns --- tests/optimize/test_backtest_detail.py | 3 +- tests/optimize/test_backtesting.py | 13 +++++++- tests/optimize/test_hyperopt.py | 45 ++++++++++++++------------ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3655b941d..7da7709c6 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -501,13 +501,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: # Dummy data as we mock the analyze functions data_processed = {pair: frame.copy()} min_date, max_date = get_timerange({pair: frame}) - results = backtesting.backtest( + result = backtesting.backtest( processed=data_processed, start_date=min_date, end_date=max_date, max_open_trades=10, ) + results = result['results'] assert len(results) == len(data.trades) assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index e09f02c66..5dfc9dbc3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -804,6 +804,12 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): + default_conf['ask_strategy'].update({ + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + }) patch_exchange(mocker) backtestmock = MagicMock(return_value={ 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), @@ -872,7 +878,12 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): - + default_conf['ask_strategy'].update({ + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + }) patch_exchange(mocker) result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], 'profit_ratio': [0.0, 0.0], diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a80f3dced..c65e90237 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -590,26 +590,31 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'hyperopt_min_trades': 1, }) - backtest_result = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", - "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], - "trade_duration": [123, 34, 31, 14], - "is_open": [False, False, False, True], - "stake_amount": [0.01, 0.01, 0.01, 0.01], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - }) + backtest_result = { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': hyperopt_conf, + 'locks': [], + 'final_balance': 1000, + } mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result) mocker.patch('freqtrade.optimize.hyperopt.get_timerange', From 97478abb9da71fbff2223cfddd8ca03765618834 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 09:05:46 +0200 Subject: [PATCH 0356/1386] Move format explanation string to HyperoptTools --- freqtrade/optimize/hyperopt.py | 17 +---------------- freqtrade/optimize/hyperopt_tools.py | 16 ++++++++++++++++ tests/optimize/test_hyperopt.py | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 229b35dd5..b69e0781a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,7 +4,6 @@ This module contains the hyperopt logic """ -import locale import logging import random import warnings @@ -301,7 +300,7 @@ class Hyperopt: strat_stats = generate_strategy_stats(processed, '', backtesting_results, min_date, max_date, market_change=0) - results_explanation = self._format_results_explanation_string( + results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) trade_count = strat_stats['total_trades'] @@ -326,20 +325,6 @@ class Hyperopt: 'total_profit': total_profit, } - def _format_results_explanation_string(self, results_metrics: Dict, stake_currency: str) -> str: - """ - Return the formatted results explanation in a string - """ - return (f"{results_metrics['total_trades']:6d} trades. " - f"{results_metrics['wins']}/{results_metrics['draws']}" - f"/{results_metrics['losses']} Wins/Draws/Losses. " - f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " - f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " - f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " - f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " - f"Avg duration {results_metrics['holding_avg']} min." - ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') - def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: return Optimizer( dimensions, diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 1635c0588..98df32c36 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -1,5 +1,6 @@ import io +import locale import logging from collections import OrderedDict from pathlib import Path @@ -145,6 +146,21 @@ class HyperoptTools(): def is_best_loss(results, current_best_loss: float) -> bool: return results['loss'] < current_best_loss + @staticmethod + def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str: + """ + Return the formatted results explanation in a string + """ + return (f"{results_metrics['total_trades']:6d} trades. " + f"{results_metrics['wins']}/{results_metrics['draws']}" + f"/{results_metrics['losses']} Wins/Draws/Losses. " + f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " + f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " + f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " + f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " + f"Avg duration {results_metrics['holding_avg']} min." + ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') + @staticmethod def _format_explanation_string(results, total_epochs) -> str: return (("*" if results['is_initial_point'] else " ") + diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c65e90237..142fc5728 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -468,7 +468,7 @@ def test_hyperopt_format_results(hyperopt): Arrow(2017, 11, 14, 19, 32, 00), Arrow(2017, 12, 14, 19, 32, 00), market_change=0) - results_explanation = hyperopt._format_results_explanation_string(results_metrics, 'BTC') + results_explanation = HyperoptTools.format_results_explanation_string(results_metrics, 'BTC') total_profit = results_metrics['profit_total_abs'] results = { From 420e75af65303d2962c37edb5a3bbd987b0f54b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 11:24:32 +0200 Subject: [PATCH 0357/1386] Extract show_backtest_result for one strategy --- freqtrade/optimize/optimize_reports.py | 60 ++++++++++++++------------ 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ef5426c9a..9a60c7c4b 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -567,37 +567,43 @@ def text_table_add_metrics(strat_results: Dict) -> str: return message +def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str): + """ + Print results for one strategy + """ + # Print results + print(f"Result for strategy {strategy}") + table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency) + if isinstance(table, str): + print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], + stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_add_metrics(results) + if isinstance(table, str) and len(table) > 0: + print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) + print(table) + + if isinstance(table, str) and len(table) > 0: + print('=' * len(table.splitlines()[0])) + print() + + def show_backtest_results(config: Dict, backtest_stats: Dict): stake_currency = config['stake_currency'] for strategy, results in backtest_stats['strategy'].items(): - - # Print results - print(f"Result for strategy {strategy}") - table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency) - if isinstance(table, str): - print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], - stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_add_metrics(results) - if isinstance(table, str) and len(table) > 0: - print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) - print(table) - - if isinstance(table, str) and len(table) > 0: - print('=' * len(table.splitlines()[0])) - print() + show_backtest_result(strategy, results, stake_currency) if len(backtest_stats['strategy']) > 1: # Print Strategy summary table From 881cba336a10e69741ab9a4c704f24b568f8b927 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 13:32:34 +0200 Subject: [PATCH 0358/1386] Show backtesting result in hyperopt-show --- freqtrade/commands/hyperopt_commands.py | 7 +++++++ freqtrade/optimize/hyperopt.py | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 268e3eeef..322729547 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -7,6 +7,7 @@ from colorama import init as colorama_init from freqtrade.configuration import setup_utils_configuration from freqtrade.data.btanalysis import get_latest_hyperopt_file from freqtrade.exceptions import OperationalException +from freqtrade.optimize.optimize_reports import show_backtest_result from freqtrade.state import RunMode @@ -125,6 +126,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: if epochs: val = epochs[n] + + metrics = val['results_metrics'] + if 'strategy_name' in metrics: + show_backtest_result(metrics['strategy_name'], metrics, + metrics['stake_currency']) + HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, header_str="Epoch details") diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b69e0781a..861d1872f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -298,8 +298,10 @@ class Hyperopt: def _get_results_dict(self, backtesting_results, min_date, max_date, params_dict, params_details, processed: Dict[str, DataFrame]): - strat_stats = generate_strategy_stats(processed, '', backtesting_results, - min_date, max_date, market_change=0) + strat_stats = generate_strategy_stats( + processed, self.backtesting.strategy.get_strategy_name(), + backtesting_results, min_date, max_date, market_change=0 + ) results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) From ecdfb6e5ed68bcec6f91e2b3b8dcf8f13603aa20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 13:32:53 +0200 Subject: [PATCH 0359/1386] Fix output of % for new format --- freqtrade/optimize/hyperopt_tools.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 98df32c36..0fdd07d83 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -157,7 +157,7 @@ class HyperoptTools(): f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " - f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " + f"({results_metrics['profit_total'] * 100: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " f"Avg duration {results_metrics['holding_avg']} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') @@ -184,8 +184,10 @@ class HyperoptTools(): if 'results_metrics.winsdrawslosses' not in trials.columns: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' + legacy_mode = True if 'results_metrics.total_trades' in trials: + legacy_mode = False # New mode, using backtest result for metrics trials['results_metrics.winsdrawslosses'] = trials.apply( lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " @@ -212,12 +214,12 @@ class HyperoptTools(): trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials['Trades'] = trials['Trades'].astype(str) - + perc_multi = 1 if legacy_mode else 100 trials['Epoch'] = trials['Epoch'].apply( lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: f'{x:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x * perc_multi:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') ) trials['Avg duration'] = trials['Avg duration'].apply( lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}" @@ -231,7 +233,7 @@ class HyperoptTools(): trials['Profit'] = trials.apply( lambda x: '{} {}'.format( round_coin_value(x['Total profit'], stake_currency), - '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') + '({:,.2f}%)'.format(x['Profit'] * perc_multi).rjust(10, ' ') ).rjust(25+len(stake_currency)) if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), axis=1 From ced5cc7ce21f5e79196bb3865baff1908c2e936f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 13:33:12 +0200 Subject: [PATCH 0360/1386] Don't recalculate min/max date - they won't change between epochs --- freqtrade/optimize/hyperopt.py | 16 +++++++--------- tests/optimize/test_hyperopt.py | 2 ++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 861d1872f..d6c5cbf93 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -275,12 +275,10 @@ class Hyperopt: processed = load(self.data_pickle_file) - min_date, max_date = get_timerange(processed) - bt_results = self.backtesting.backtest( processed=processed, - start_date=min_date.datetime, - end_date=max_date.datetime, + start_date=self.min_date.datetime, + end_date=self.max_date.datetime, max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, enable_protections=self.config.get('enable_protections', False), @@ -291,7 +289,7 @@ class Hyperopt: 'backtest_end_time': int(backtest_end_time.timestamp()), }) - return self._get_results_dict(bt_results, min_date, max_date, + return self._get_results_dict(bt_results, self.min_date, self.max_date, params_dict, params_details, processed=processed) @@ -357,11 +355,11 @@ class Hyperopt: for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange, startup_candles=self.backtesting.required_startup) - min_date, max_date = get_timerange(preprocessed) + self.min_date, self.max_date = get_timerange(preprocessed) - logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') + logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'({(self.max_date - self.min_date).days} days)..') dump(preprocessed, self.data_pickle_file) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 142fc5728..554f525a8 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -693,6 +693,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: } hyperopt = Hyperopt(hyperopt_conf) + hyperopt.min_date = Arrow(2017, 12, 10) + hyperopt.max_date = Arrow(2017, 12, 13) hyperopt.dimensions = hyperopt.hyperopt_space() generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected From 46f0f6603990194b2c5dd1195b0fdb01975ef914 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 09:35:10 +0200 Subject: [PATCH 0361/1386] Keep dimensions stored in hyperopt class There is no point in regenerating them and it will cause some overhead as all space classes will be recreated for every epoch. --- freqtrade/optimize/hyperopt.py | 63 ++++++++++++++++----------------- tests/optimize/test_hyperopt.py | 3 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d6c5cbf93..4e32c2ecb 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -65,6 +65,13 @@ class Hyperopt: custom_hyperopt: IHyperOpt def __init__(self, config: Dict[str, Any]) -> None: + self.buy_space: List[Dimension] = [] + self.sell_space: List[Dimension] = [] + self.roi_space: List[Dimension] = [] + self.stoploss_space: List[Dimension] = [] + self.trailing_space: List[Dimension] = [] + self.dimensions: List[Dimension] = [] + self.config = config self.backtesting = Backtesting(self.config) @@ -139,9 +146,7 @@ class Hyperopt: logger.info(f"Removing `{p}`.") p.unlink() - def _get_params_dict(self, raw_params: List[Any]) -> Dict: - - dimensions: List[Dimension] = self.dimensions + def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: # Ensure the number of dimensions match # the number of parameters in the list. @@ -175,16 +180,13 @@ class Hyperopt: result: Dict = {} if HyperoptTools.has_space(self.config, 'buy'): - result['buy'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('buy')} + result['buy'] = {p.name: params.get(p.name) for p in self.buy_space} if HyperoptTools.has_space(self.config, 'sell'): - result['sell'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('sell')} + result['sell'] = {p.name: params.get(p.name) for p in self.sell_space} if HyperoptTools.has_space(self.config, 'roi'): result['roi'] = self.custom_hyperopt.generate_roi_table(params) if HyperoptTools.has_space(self.config, 'stoploss'): - result['stoploss'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('stoploss')} + result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} if HyperoptTools.has_space(self.config, 'trailing'): result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) @@ -207,38 +209,32 @@ class Hyperopt: ) self.hyperopt_table_header = 2 - def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: + def init_spaces(self): """ - Return the dimensions in the hyperoptimization space. - :param space: Defines hyperspace to return dimensions for. - If None, then the self.has_space() will be used to return dimensions - for all hyperspaces used. + Assign the dimensions in the hyperoptimization space. """ - spaces: List[Dimension] = [] - if space == 'buy' or (space is None and HyperoptTools.has_space(self.config, 'buy')): + if HyperoptTools.has_space(self.config, 'buy'): logger.debug("Hyperopt has 'buy' space") - spaces += self.custom_hyperopt.indicator_space() + self.buy_space = self.custom_hyperopt.indicator_space() - if space == 'sell' or (space is None and HyperoptTools.has_space(self.config, 'sell')): + if HyperoptTools.has_space(self.config, 'sell'): logger.debug("Hyperopt has 'sell' space") - spaces += self.custom_hyperopt.sell_indicator_space() + self.sell_space = self.custom_hyperopt.sell_indicator_space() - if space == 'roi' or (space is None and HyperoptTools.has_space(self.config, 'roi')): + if HyperoptTools.has_space(self.config, 'roi'): logger.debug("Hyperopt has 'roi' space") - spaces += self.custom_hyperopt.roi_space() + self.roi_space = self.custom_hyperopt.roi_space() - if space == 'stoploss' or (space is None - and HyperoptTools.has_space(self.config, 'stoploss')): + if HyperoptTools.has_space(self.config, 'stoploss'): logger.debug("Hyperopt has 'stoploss' space") - spaces += self.custom_hyperopt.stoploss_space() + self.stoploss_space = self.custom_hyperopt.stoploss_space() - if space == 'trailing' or (space is None - and HyperoptTools.has_space(self.config, 'trailing')): + if HyperoptTools.has_space(self.config, 'trailing'): logger.debug("Hyperopt has 'trailing' space") - spaces += self.custom_hyperopt.trailing_space() - - return spaces + self.trailing_space = self.custom_hyperopt.trailing_space() + self.dimensions = (self.buy_space + self.sell_space + self.roi_space + + self.stoploss_space + self.trailing_space) def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: """ @@ -246,9 +242,10 @@ class Hyperopt: Keep this function as optimized as possible! """ backtest_start_time = datetime.now(timezone.utc) - params_dict = self._get_params_dict(raw_params) + params_dict = self._get_params_dict(self.dimensions, raw_params) params_details = self._get_params_details(params_dict) + # Apply parameters if HyperoptTools.has_space(self.config, 'roi'): self.backtesting.strategy.minimal_roi = ( # type: ignore self.custom_hyperopt.generate_roi_table(params_dict)) @@ -294,7 +291,8 @@ class Hyperopt: processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, - params_dict, params_details, processed: Dict[str, DataFrame]): + params_dict, params_details, processed: Dict[str, DataFrame] + ) -> Dict[str, Any]: strat_stats = generate_strategy_stats( processed, self.backtesting.strategy.get_strategy_name(), @@ -347,6 +345,8 @@ class Hyperopt: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) logger.info(f"Using optimizer random state: {self.random_state}") self.hyperopt_table_header = -1 + # Initialize spaces ... + self.init_spaces() data, timerange = self.backtesting.load_bt_data() logger.info("Dataload complete. Calculating indicators") preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) @@ -377,7 +377,6 @@ class Hyperopt: config_jobs = self.config.get('hyperopt_jobs', -1) logger.info(f'Number of parallel jobs set as: {config_jobs}') - self.dimensions: List[Dimension] = self.hyperopt_space() self.opt = self.get_optimizer(self.dimensions, config_jobs) if self.print_colorized: diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 554f525a8..82bb4fdd2 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -695,7 +695,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt = Hyperopt(hyperopt_conf) hyperopt.min_date = Arrow(2017, 12, 10) hyperopt.max_date = Arrow(2017, 12, 13) - hyperopt.dimensions = hyperopt.hyperopt_space() + hyperopt.init_spaces() + hyperopt.dimensions = hyperopt.dimensions generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected From 6b6270db132ebb41dc334aaf8076588c3f368efa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 10:37:54 +0200 Subject: [PATCH 0362/1386] Add hint about "sell_profit_only" to docs --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7fa889dd1..eb322df9d 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -51,7 +51,7 @@ For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. You should abstain from using custom_sell() signals in place of stoplosses though. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. !!! Note - Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. + Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: From 9049d6b779c39a697e755e5e69c48c3eb1994d48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 10:45:21 +0200 Subject: [PATCH 0363/1386] Reformat hyper to cache parameters --- freqtrade/strategy/hyper.py | 43 +++++++++++++++++++++++++++++--- tests/strategy/test_interface.py | 2 +- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 2714ffb43..7dee47d87 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -5,7 +5,7 @@ This module defines a base class for auto-hyperoptable strategies. import logging from abc import ABC, abstractmethod from contextlib import suppress -from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -29,6 +29,7 @@ class BaseParameter(ABC): default: Any value: Any in_space: bool = False + name: str def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -250,6 +251,9 @@ class HyperStrategyMixin(object): Initialize hyperoptable strategy mixin. """ self.config = config + self.ft_buy_params: List[BaseParameter] = [] + self.ft_sell_params: List[BaseParameter] = [] + self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: @@ -260,15 +264,26 @@ class HyperStrategyMixin(object): """ if category not in ('buy', 'sell', None): raise OperationalException('Category must be one of: "buy", "sell", None.') + + if category is None: + params = self.ft_buy_params + self.ft_sell_params + else: + params = getattr(self, f"ft_{category}_params") + + 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 (category and attr_name.startswith(category + '_') + 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 is None or category == attr.category or + if (category == attr.category or (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr @@ -286,9 +301,16 @@ class HyperStrategyMixin(object): """ if not params: 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.enumerate_parameters(space): + for attr_name, attr in self._detect_parameters(space): + attr.name = attr_name attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space) + if not attr.category: + attr.category = space + + param_container.append(attr) + if params and attr_name in params: if attr.load: attr.value = params[attr_name] @@ -298,3 +320,16 @@ class HyperStrategyMixin(object): f'Default value "{attr.value}" used.') else: logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') + + def get_params_dict(self): + """ + Returns list of Parameters that are not part of the current optimize job + """ + params = { + 'buy': {}, + 'sell': {} + } + for name, p in self.enumerate_parameters(): + if not p.optimize or not p.in_space: + params[p.category][name] = p.value + return params diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4ca514349..a241d7f43 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -671,4 +671,4 @@ def test_auto_hyperopt_interface(default_conf): strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy') with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): - [x for x in strategy.enumerate_parameters('sell')] + [x for x in strategy._detect_parameters('sell')] From 8ee0b0d8e85228cdade883066edef1d29af2a0c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 10:46:04 +0200 Subject: [PATCH 0364/1386] Store not optimized parameters (if applicable) --- freqtrade/optimize/hyperopt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 4e32c2ecb..ec3c6f385 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -301,6 +301,8 @@ class Hyperopt: results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) + not_optimized = self.backtesting.strategy.get_params_dict() + trade_count = strat_stats['total_trades'] total_profit = strat_stats['profit_total'] @@ -318,6 +320,7 @@ class Hyperopt: 'loss': loss, 'params_dict': params_dict, 'params_details': params_details, + 'params_not_optimized': not_optimized, 'results_metrics': strat_stats, 'results_explanation': results_explanation, 'total_profit': total_profit, From d069ad43d83eae4215409e68fd3cc8626fb32983 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 11:01:26 +0200 Subject: [PATCH 0365/1386] Small reformatting in hyperopt --- freqtrade/optimize/hyperopt.py | 6 +++--- tests/optimize/test_hyperopt.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ec3c6f385..639e9fb93 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -243,7 +243,6 @@ class Hyperopt: """ backtest_start_time = datetime.now(timezone.utc) params_dict = self._get_params_dict(self.dimensions, raw_params) - params_details = self._get_params_details(params_dict) # Apply parameters if HyperoptTools.has_space(self.config, 'roi'): @@ -287,12 +286,13 @@ class Hyperopt: }) return self._get_results_dict(bt_results, self.min_date, self.max_date, - params_dict, params_details, + params_dict, processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, - params_dict, params_details, processed: Dict[str, DataFrame] + params_dict, processed: Dict[str, DataFrame] ) -> Dict[str, Any]: + params_details = self._get_params_details(params_dict) strat_stats = generate_strategy_stats( processed, self.backtesting.strategy.get_strategy_name(), diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 82bb4fdd2..774dd35a4 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -688,6 +688,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_stop_positive': 0.02, 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, + 'params_not_optimized': {'buy': {}, 'sell': {}}, 'results_metrics': ANY, 'total_profit': 3.1e-08 } From 287b43e999b89da122e052014b2b0fd53297de7c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 11:30:53 +0200 Subject: [PATCH 0366/1386] Output strategy results including non-optimized parameters --- freqtrade/optimize/hyperopt_tools.py | 54 +++++++++++++++++++--------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 0fdd07d83..724f451c5 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -4,7 +4,6 @@ import locale import logging from collections import OrderedDict from pathlib import Path -from pprint import pformat from typing import Any, Dict, List import rapidjson @@ -66,6 +65,7 @@ class HyperoptTools(): Display details of the hyperopt result """ params = results.get('params_details', {}) + non_optimized = results.get('params_not_optimized', {}) # Default header string if header_str is None: @@ -82,8 +82,10 @@ class HyperoptTools(): print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: - HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:") - HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:") + HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:", + non_optimized) + HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:", + non_optimized) HyperoptTools._params_pretty_print(params, 'roi', "ROI table:") HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") @@ -109,12 +111,12 @@ class HyperoptTools(): result_dict.update(space_params) @staticmethod - def _params_pretty_print(params, space: str, header: str) -> None: - if space in params: + def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None: + if space in params or space in non_optimized: space_params = HyperoptTools._space_params(params, space, 5) - params_result = f"\n# {header}\n" + result = f"\n# {header}\n" if space == 'stoploss': - params_result += f"stoploss = {space_params.get('stoploss')}" + result += f"stoploss = {space_params.get('stoploss')}" elif space == 'roi': # TODO: get rid of OrderedDict when support for python 3.6 will be # dropped (dicts keep the order as the language feature) @@ -123,24 +125,44 @@ class HyperoptTools(): (str(k), v) for k, v in space_params.items() ), default=str, indent=4, number_mode=rapidjson.NM_NATIVE) - params_result += f"minimal_roi = {minimal_roi_result}" + result += f"minimal_roi = {minimal_roi_result}" elif space == 'trailing': for k, v in space_params.items(): - params_result += f'{k} = {v}\n' + result += f'{k} = {v}\n' else: - params_result += f"{space}_params = {pformat(space_params, indent=4)}" - params_result = params_result.replace("}", "\n}").replace("{", "{\n ") + no_params = HyperoptTools._space_params(non_optimized, space, 5) - params_result = params_result.replace("\n", "\n ") - print(params_result) + result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}" + + result = result.replace("\n", "\n ") + print(result) @staticmethod def _space_params(params, space: str, r: int = None) -> Dict: - d = params[space] - # Round floats to `r` digits after the decimal point if requested - return round_dict(d, r) if r else d + d = params.get(space) + if d: + # Round floats to `r` digits after the decimal point if requested + return round_dict(d, r) if r else d + return {} + + @staticmethod + def _pprint(params, non_optimized, indent: int = 4): + """ + Pretty-print hyperopt results (based on 2 dicts - with add. comment) + """ + p = params.copy() + p.update(non_optimized) + result = '{\n' + + for k, param in p.items(): + result += " " * indent + f'"{k}": {param},' + if k in non_optimized: + result += " # value loaded from strategy" + result += "\n" + result += '}' + return result @staticmethod def is_best_loss(results, current_best_loss: float) -> bool: From 4fc37f15d106855b1bc945d33b2ce5119ea8f9fe Mon Sep 17 00:00:00 2001 From: youpas Date: Sun, 2 May 2021 11:41:26 +0200 Subject: [PATCH 0367/1386] Fixed syntax error in the example Removed extra comma in the "Full example of Pairlist Handlers" section. --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 85d157e75..2cd6feca0 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -204,7 +204,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, { "method": "VolumePairList", "number_assets": 20, - "sort_key": "quoteVolume", + "sort_key": "quoteVolume" }, {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, From b71d8505965624a308d6576a52892a6d7d17494f Mon Sep 17 00:00:00 2001 From: youpas Date: Sun, 2 May 2021 11:47:02 +0200 Subject: [PATCH 0368/1386] Fixed anchor link for PriceFilter --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 85d157e75..d653303f6 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -193,7 +193,7 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove ### Full example of Pairlist Handlers -The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#pricefilter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. ```json "exchange": { From 99e1ef9b4a427b28953b35ae273f448bc60ef6be Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 19:21:26 +0200 Subject: [PATCH 0369/1386] Fix docs typo for CategoryParameter closes #4852 --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b3fdc699b..5f1f9bffa 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -251,9 +251,9 @@ We continue to define hyperoptable parameters: class MyAwesomeStrategy(IStrategy): buy_adx = IntParameter(20, 40, default=30) buy_rsi = IntParameter(20, 40, default=30) - buy_adx_enabled = CategoricalParameter([True, False]), - buy_rsi_enabled = CategoricalParameter([True, False]), - buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), + buy_adx_enabled = CategoricalParameter([True, False]) + buy_rsi_enabled = CategoricalParameter([True, False]) + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']) ``` Above definition says: I have five parameters I want to randomly combine to find the best combination. From ef9dd0676cf0de365967d4e0108162957dc100bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 20:06:47 +0200 Subject: [PATCH 0370/1386] Rename hyperoptresult fixture to avoid naming collision --- tests/commands/test_commands.py | 8 ++++---- tests/conftest.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d86bced5d..9195a1a92 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -918,10 +918,10 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): captured.out) -def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=hyperopt_results) + MagicMock(return_value=saved_hyperopt_results) ) args = [ @@ -1150,10 +1150,10 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): f.unlink() -def test_hyperopt_show(mocker, capsys, hyperopt_results): +def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=hyperopt_results) + MagicMock(return_value=saved_hyperopt_results) ) args = [ diff --git a/tests/conftest.py b/tests/conftest.py index 788586134..ffb09cc60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1778,7 +1778,7 @@ def open_trade(): @pytest.fixture -def hyperopt_results(): +def saved_hyperopt_results(): return [ { 'loss': 0.4366182531161519, From 303895b33e3a0931dd4dc26c687157a5fa080a9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 20:07:22 +0200 Subject: [PATCH 0371/1386] Add support for filters to new hyperopt-results --- freqtrade/commands/hyperopt_commands.py | 71 ++++++++++++++++++------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 322729547..cf3bc3005 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -139,11 +139,13 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: """ Filter our items from the list of hyperopt results + TODO: after 2021.5 remove all "legacy" mode queries. """ if filteroptions['only_best']: epochs = [x for x in epochs if x['is_best']] if filteroptions['only_profitable']: - epochs = [x for x in epochs if x['results_metrics']['profit'] > 0] + epochs = [x for x in epochs if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0)) > 0] epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) @@ -160,34 +162,55 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: return epochs +def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int): + """ + Filter epochs with trade-counts > trades + """ + return [ + x for x in epochs + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades', 0) + ) > trade_count + ] + + def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_trades'] > 0: - epochs = [ - x for x in epochs - if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades'] - ] + epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades']) + if filteroptions['filter_max_trades'] > 0: epochs = [ x for x in epochs - if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades'] + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades') + ) < filteroptions['filter_max_trades'] ] return epochs def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: + def get_duration_value(x): + # Duration in minutes ... + if 'duration' in x['results_metrics']: + return x['results_metrics']['duration'] + else: + # New mode + avg = x['results_metrics']['holding_avg'] + return avg.total_seconds() // 60 + if filteroptions['filter_min_avg_time'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time'] + if get_duration_value(x) > filteroptions['filter_min_avg_time'] ] if filteroptions['filter_max_avg_time'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time'] + if get_duration_value(x) < filteroptions['filter_max_avg_time'] ] return epochs @@ -196,28 +219,36 @@ def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_avg_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['avg_profit'] > filteroptions['filter_min_avg_profit'] + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) + ) > filteroptions['filter_min_avg_profit'] ] if filteroptions['filter_max_avg_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['avg_profit'] < filteroptions['filter_max_avg_profit'] + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) + ) < filteroptions['filter_max_avg_profit'] ] if filteroptions['filter_min_total_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit'] + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0) + ) > filteroptions['filter_min_total_profit'] ] if filteroptions['filter_max_total_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit'] + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0) + ) < filteroptions['filter_max_total_profit'] ] return epochs @@ -225,11 +256,11 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_objective'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] if filteroptions['filter_max_objective'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] From fc110ea418719661052616378adb882463e3ff0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 20:41:45 +0200 Subject: [PATCH 0372/1386] Support csv export for new and old versions --- freqtrade/commands/hyperopt_commands.py | 8 +- freqtrade/optimize/hyperopt_tools.py | 33 ++++-- tests/conftest.py | 137 +++++++++++++++++++++++- 3 files changed, 162 insertions(+), 16 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index cf3bc3005..e072e12cb 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -223,7 +223,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = [ x for x in epochs if x['results_metrics'].get( - 'avg_profit', x['results_metrics'].get('profit_mean', 0) + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 ) > filteroptions['filter_min_avg_profit'] ] if filteroptions['filter_max_avg_profit'] is not None: @@ -231,7 +231,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = [ x for x in epochs if x['results_metrics'].get( - 'avg_profit', x['results_metrics'].get('profit_mean', 0) + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 ) < filteroptions['filter_max_avg_profit'] ] if filteroptions['filter_min_total_profit'] is not None: @@ -239,7 +239,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = [ x for x in epochs if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total', 0) + 'profit', x['results_metrics'].get('profit_total_abs', 0) ) > filteroptions['filter_min_total_profit'] ] if filteroptions['filter_max_total_profit'] is not None: @@ -247,7 +247,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = [ x for x in epochs if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total', 0) + 'profit', x['results_metrics'].get('profit_total_abs', 0) ) < filteroptions['filter_max_total_profit'] ] return epochs diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 724f451c5..ee3366a4e 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -318,11 +318,21 @@ class HyperoptTools(): trials['Best'] = '' trials['Stake currency'] = config['stake_currency'] - base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', - 'results_metrics.total_profit', - 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best'] + if 'results_metrics.total_trades' in trials: + base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', + 'results_metrics.profit_mean', 'results_metrics.profit_median', + 'results_metrics.profit_total', + 'Stake currency', + 'results_metrics.profit_total_abs', 'results_metrics.holding_avg', + 'loss', 'is_initial_point', 'is_best'] + perc_multi = 100 + else: + perc_multi = 1 + base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.avg_profit', 'results_metrics.median_profit', + 'results_metrics.total_profit', + 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best'] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] trials = trials[base_metrics + param_metrics] @@ -339,21 +349,24 @@ class HyperoptTools(): trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials['Epoch'] = trials['Epoch'].astype(str) trials['Trades'] = trials['Trades'].astype(str) + trials['Median profit'] = trials['Median profit'] * perc_multi trials['Total profit'] = trials['Total profit'].apply( - lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" + lambda x: f'{x:,.8f}' if x != 0.0 else "" ) trials['Profit'] = trials['Profit'].apply( - lambda x: '{:,.2f}'.format(x) if not isna(x) else "" + lambda x: f'{x:,.2f}' if not isna(x) else "" ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" + lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" ) trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" + lambda x: f'{x:,.1f} m' if isinstance( + x, float) else f"{x.total_seconds() // 60:,.1f} m" + if not isna(x) else "" ) trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x) if x != 100000 else "" + lambda x: f'{x:,.5f}' if x != 100000 else "" ) trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) diff --git a/tests/conftest.py b/tests/conftest.py index ffb09cc60..83a34359a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import json import logging import re from copy import deepcopy -from datetime import datetime +from datetime import datetime, timedelta from functools import reduce from pathlib import Path from unittest.mock import MagicMock, Mock, PropertyMock @@ -1778,7 +1778,7 @@ def open_trade(): @pytest.fixture -def saved_hyperopt_results(): +def saved_hyperopt_results_legacy(): return [ { 'loss': 0.4366182531161519, @@ -1907,3 +1907,136 @@ def saved_hyperopt_results(): 'is_best': False } ] + + +@pytest.fixture +def saved_hyperopt_results(): + return [ + { + 'loss': 0.4366182531161519, + 'params_dict': { + 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 + 'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0)}, # noqa: E501 + 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 + 'total_profit': -0.00125625, + 'current_epoch': 1, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 20.0, + 'params_dict': { + 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501 + 'params_details': { + 'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501 + 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 + 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.338070047333259}}, + 'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 + 'total_profit': 6.185e-05, + 'current_epoch': 2, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 14.241196856510731, + 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 + 'results_metrics': {'total_trades': 621, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501 + 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 + 'total_profit': -0.13639474, + 'current_epoch': 3, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 + 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False + }, { + 'loss': 0.22195522184191518, + 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 + 'results_metrics': {'total_trades': 14, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501 + 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 + 'total_profit': -0.002480140000000001, + 'current_epoch': 5, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 0.545315889154162, + 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 + 'results_metrics': {'total_trades': 39, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501 + 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 + 'total_profit': -0.0041773, + 'current_epoch': 6, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 4.713497421432944, + 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501 + 'params_details': { + 'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 + 'results_metrics': {'total_trades': 318, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501 + 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 + 'total_profit': -0.06339929, + 'current_epoch': 7, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 20.0, # noqa: E501 + 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 + 'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 + 'total_profit': 0.0, + 'current_epoch': 8, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 2.4731817780991223, + 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 + 'results_metrics': {'total_trades': 229, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501 + 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 + 'total_profit': -0.044050070000000004, # noqa: E501 + 'current_epoch': 9, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': -0.2604606005845212, # noqa: E501 + 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 + 'results_metrics': {'total_trades': 4, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501 + 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 + 'total_profit': 0.00021629, + 'current_epoch': 10, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 4.876465945994304, # noqa: E501 + 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 + # New Hyperopt mode! + 'results_metrics': {'total_trades': 117, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501 + 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 + 'total_profit': -0.07436117, + 'current_epoch': 11, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 + 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, + 'current_epoch': 12, + 'is_initial_point': True, + 'is_best': False + } + ] From 6d7096dc665a7ffb0818ab31bba180e983307238 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 20:42:01 +0200 Subject: [PATCH 0373/1386] Use both old and new fixtures for test --- tests/commands/test_commands.py | 459 ++++++++++++++++---------------- 1 file changed, 230 insertions(+), 229 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 9195a1a92..60e6d2ea8 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -918,236 +918,237 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): captured.out) -def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results): - mocker.patch( - 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=saved_hyperopt_results) - ) +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, saved_hyperopt_results_legacy): + for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy): + mocker.patch( + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', + MagicMock(return_value=saved_hyperopt_results_legacy) + ) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", - " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--best", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 5/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", - "Sell hyperspace params", "ROI table", "Stoploss"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--max-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-profit", "0.11", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-profit", "0.10", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--max-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-time", "2000", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", - " 8/12", " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-time", "1500", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 6/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" - " 9/12", " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--export-csv", "test_file.csv", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - log_has("CSV file created: test_file.csv", caplog) - f = Path("test_file.csv") - assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() - assert f.is_file() - f.unlink() + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", + " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--best", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 5/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", + "Sell hyperspace params", "ROI table", "Stoploss"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--max-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-profit", "0.11", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-profit", "0.10", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--max-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-time", "2000", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", + " 8/12", " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-time", "1500", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 6/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" + " 9/12", " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--export-csv", "test_file.csv", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + log_has("CSV file created: test_file.csv", caplog) + f = Path("test_file.csv") + assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() + assert f.is_file() + f.unlink() def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): From da574e4e69b9afa6f76484bd09738dde2bd97ee7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 May 2021 06:30:41 +0200 Subject: [PATCH 0374/1386] Small style fixes --- freqtrade/optimize/hyperopt_tools.py | 3 +-- tests/commands/test_commands.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index ee3366a4e..f655582c4 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -362,8 +362,7 @@ class HyperoptTools(): ) trials['Avg duration'] = trials['Avg duration'].apply( lambda x: f'{x:,.1f} m' if isinstance( - x, float) else f"{x.total_seconds() // 60:,.1f} m" - if not isna(x) else "" + x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else "" ) trials['Objective'] = trials['Objective'].apply( lambda x: f'{x:,.5f}' if x != 100000 else "" diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 60e6d2ea8..4d3937d87 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -918,7 +918,8 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): captured.out) -def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, saved_hyperopt_results_legacy): +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, + saved_hyperopt_results_legacy): for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', From 8364343cd63853eaa878dd4621778ee1669a918e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:26:08 +0000 Subject: [PATCH 0375/1386] Bump scikit-learn from 0.24.1 to 0.24.2 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.24.1 to 0.24.2. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.24.1...0.24.2) 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 79ad722e2..5e7e9d9d2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.6.3 -scikit-learn==0.24.1 +scikit-learn==0.24.2 scikit-optimize==0.8.1 filelock==3.0.12 joblib==1.0.1 From 8ed15fb7ccb298130393af64ce10be1d96bfb39b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:26:18 +0000 Subject: [PATCH 0376/1386] Bump sqlalchemy from 1.4.11 to 1.4.12 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.11 to 1.4.12. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f1739a1..6b537f5e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.48.76 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.11 +SQLAlchemy==1.4.12 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 37227170b36bee2e13d062c93736ceb559795eda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:26:30 +0000 Subject: [PATCH 0377/1386] Bump pyjwt from 2.0.1 to 2.1.0 Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.0.1 to 2.1.0. - [Release notes](https://github.com/jpadilla/pyjwt/releases) - [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst) - [Commits](https://github.com/jpadilla/pyjwt/compare/2.0.1...2.1.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f1739a1..04a3c4ad8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ sdnotify==0.3.2 # API Server fastapi==0.63.0 uvicorn==0.13.4 -pyjwt==2.0.1 +pyjwt==2.1.0 aiofiles==0.6.0 # Support for colorized terminal output From cea207026aab9753e8378c3da94e8491ce3feb3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:26:39 +0000 Subject: [PATCH 0378/1386] Bump technical from 1.2.2 to 1.3.0 Bumps [technical](https://github.com/freqtrade/technical) from 1.2.2 to 1.3.0. - [Release notes](https://github.com/freqtrade/technical/releases) - [Commits](https://github.com/freqtrade/technical/compare/1.2.2...1.3.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f1739a1..6dcefcd2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 -technical==1.2.2 +technical==1.3.0 tabulate==0.8.9 pycoingecko==2.0.0 jinja2==2.11.3 From a63d9e9515c2855c310b5fcceebcf8e11e64ea3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:27:17 +0000 Subject: [PATCH 0379/1386] Bump arrow from 1.0.3 to 1.1.0 Bumps [arrow](https://github.com/arrow-py/arrow) from 1.0.3 to 1.1.0. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/1.0.3...1.1.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f1739a1..86a39c513 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ cryptography==3.4.7 aiohttp==3.7.4.post0 SQLAlchemy==1.4.11 python-telegram-bot==13.4.1 -arrow==1.0.3 +arrow==1.1.0 cachetools==4.2.1 requests==2.25.1 urllib3==1.26.4 From 1ffc53b3b5c770198bd664c534ae7a1f251a08d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 19:21:26 +0200 Subject: [PATCH 0380/1386] Fix docs typo for CategoryParameter closes #4852 --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b3fdc699b..5f1f9bffa 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -251,9 +251,9 @@ We continue to define hyperoptable parameters: class MyAwesomeStrategy(IStrategy): buy_adx = IntParameter(20, 40, default=30) buy_rsi = IntParameter(20, 40, default=30) - buy_adx_enabled = CategoricalParameter([True, False]), - buy_rsi_enabled = CategoricalParameter([True, False]), - buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), + buy_adx_enabled = CategoricalParameter([True, False]) + buy_rsi_enabled = CategoricalParameter([True, False]) + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']) ``` Above definition says: I have five parameters I want to randomly combine to find the best combination. From f138cca7975cd2cdfb6f539ba828fe04c47c0a67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 May 2021 08:33:06 +0200 Subject: [PATCH 0381/1386] Be explicit with space assignment in documentation --- docs/hyperopt.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 5f1f9bffa..d8f4a8071 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -249,11 +249,11 @@ We continue to define hyperoptable parameters: ```python class MyAwesomeStrategy(IStrategy): - buy_adx = IntParameter(20, 40, default=30) - buy_rsi = IntParameter(20, 40, default=30) - buy_adx_enabled = CategoricalParameter([True, False]) - buy_rsi_enabled = CategoricalParameter([True, False]) - buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']) + buy_adx = IntParameter(20, 40, default=30, space="buy") + buy_rsi = IntParameter(20, 40, default=30, space="buy") + buy_adx_enabled = CategoricalParameter([True, False], space="buy") + buy_rsi_enabled = CategoricalParameter([True, False], space="buy") + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal'], space="buy") ``` Above definition says: I have five parameters I want to randomly combine to find the best combination. @@ -262,6 +262,10 @@ Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. +!!! Note "Parameter space assignment" + Parameters must either be assigned to a variable named `buy_*` or `sell_*` - or contain `space='buy'` | `space='sell'` to be assigned to a space correctly. + If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt. + So let's write the buy strategy using these values: ```python From 3d11df68e32e9415146f6920746dc4722a527116 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 May 2021 08:33:06 +0200 Subject: [PATCH 0382/1386] Be explicit with space assignment in documentation --- docs/hyperopt.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 5f1f9bffa..d8f4a8071 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -249,11 +249,11 @@ We continue to define hyperoptable parameters: ```python class MyAwesomeStrategy(IStrategy): - buy_adx = IntParameter(20, 40, default=30) - buy_rsi = IntParameter(20, 40, default=30) - buy_adx_enabled = CategoricalParameter([True, False]) - buy_rsi_enabled = CategoricalParameter([True, False]) - buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']) + buy_adx = IntParameter(20, 40, default=30, space="buy") + buy_rsi = IntParameter(20, 40, default=30, space="buy") + buy_adx_enabled = CategoricalParameter([True, False], space="buy") + buy_rsi_enabled = CategoricalParameter([True, False], space="buy") + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal'], space="buy") ``` Above definition says: I have five parameters I want to randomly combine to find the best combination. @@ -262,6 +262,10 @@ Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. +!!! Note "Parameter space assignment" + Parameters must either be assigned to a variable named `buy_*` or `sell_*` - or contain `space='buy'` | `space='sell'` to be assigned to a space correctly. + If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt. + So let's write the buy strategy using these values: ```python From 82a08bd7def9d1661a3804c32960abe630d1626f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 10:16:58 +0000 Subject: [PATCH 0383/1386] Bump python-telegram-bot from 13.4.1 to 13.5 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.4.1 to 13.5. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.4.1...v13.5) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 91d760c9e..d4fe5bdd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.48.76 cryptography==3.4.7 aiohttp==3.7.4.post0 SQLAlchemy==1.4.12 -python-telegram-bot==13.4.1 +python-telegram-bot==13.5 arrow==1.1.0 cachetools==4.2.1 requests==2.25.1 From 2d8982426711e8a11a256c00121bccca34dfa664 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 10:21:49 +0000 Subject: [PATCH 0384/1386] Bump cachetools from 4.2.1 to 4.2.2 Bumps [cachetools](https://github.com/tkem/cachetools) from 4.2.1 to 4.2.2. - [Release notes](https://github.com/tkem/cachetools/releases) - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v4.2.1...v4.2.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 91d760c9e..08e13525d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.7.4.post0 SQLAlchemy==1.4.12 python-telegram-bot==13.4.1 arrow==1.1.0 -cachetools==4.2.1 +cachetools==4.2.2 requests==2.25.1 urllib3==1.26.4 wrapt==1.12.1 From 860379bc58262f69735aceb018850476abcf0db1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 10:23:52 +0000 Subject: [PATCH 0385/1386] Bump ccxt from 1.48.76 to 1.49.30 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.48.76 to 1.49.30. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.48.76...1.49.30) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 91d760c9e..d4f89be5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.4 -ccxt==1.48.76 +ccxt==1.49.30 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From da47f4e1a46a4c4982f3b1e67f39eb30516c9c59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 06:47:26 +0200 Subject: [PATCH 0386/1386] Fix Kraken balance update error closes #4873 --- freqtrade/exchange/kraken.py | 2 ++ tests/exchange/test_kraken.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 786f1b592..6f1fa409a 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -53,6 +53,8 @@ class Kraken(Exchange): # x["side"], x["amount"], ) for x in orders] for bal in balances: + if not isinstance(balances[bal], dict): + continue balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal) balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 97f428e2f..ed22cde92 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -90,6 +90,7 @@ def test_get_balances_prod(default_conf, mocker): '3ST': balance_item.copy(), '4ST': balance_item.copy(), 'EUR': balance_item.copy(), + 'timestamp': 123123 }) kraken_open_orders = [{'symbol': '1ST/EUR', 'type': 'limit', @@ -138,7 +139,7 @@ def test_get_balances_prod(default_conf, mocker): default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") balances = exchange.get_balances() - assert len(balances) == 5 + assert len(balances) == 6 assert balances['1ST']['free'] == 9.0 assert balances['1ST']['total'] == 10.0 From f55ce04fa6a8aa41cdfc66c0c3cfcd1cd5adde46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 May 2021 05:14:42 +0000 Subject: [PATCH 0387/1386] Bump python from 3.9.4-slim-buster to 3.9.5-slim-buster Bumps python from 3.9.4-slim-buster to 3.9.5-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 128d0e19c..7d5afac9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.4-slim-buster as base +FROM python:3.9.5-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 2b3bca042..9b825d126 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.5-slim-buster as base # Setup env ENV LANG C.UTF-8 From 947ad856c066961da956ebc2682c0b12a4a6a765 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 08:12:28 +0200 Subject: [PATCH 0388/1386] Update Dockerfile.armhf --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 9b825d126..2b3bca042 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.5-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 From 431cb5313f7c191b70e895faf4c422db310654f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 07:37:21 +0200 Subject: [PATCH 0389/1386] Support informative pairs in edge positioning --- freqtrade/edge/edge_positioning.py | 34 ++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 334aabfab..721e22262 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -1,6 +1,8 @@ # pragma pylint: disable=W0603 """ Edge positioning package """ import logging +from collections import defaultdict +from copy import deepcopy from typing import Any, Dict, List, NamedTuple import arrow @@ -12,8 +14,10 @@ from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.exceptions import OperationalException +from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist -from freqtrade.strategy.interface import SellType +from freqtrade.state import RunMode +from freqtrade.strategy.interface import IStrategy, SellType logger = logging.getLogger(__name__) @@ -45,7 +49,7 @@ class Edge: self.config = config self.exchange = exchange - self.strategy = strategy + self.strategy: IStrategy = strategy self.edge_config = self.config.get('edge', {}) self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs @@ -102,14 +106,33 @@ class Edge: logger.info('Using local backtesting data (using whitelist in given config) ...') if self._refresh_pairs: + timerange_startup = deepcopy(self._timerange) + timerange_startup.subtract_start(timeframe_to_seconds( + self.strategy.timeframe) * self.strategy.startup_candle_count) refresh_data( datadir=self.config['datadir'], pairs=pairs, exchange=self.exchange, timeframe=self.strategy.timeframe, - timerange=self._timerange, + timerange=timerange_startup, data_format=self.config.get('dataformat_ohlcv', 'json'), ) + # Download informative pairs too + res = defaultdict(list) + for p, t in self.strategy.informative_pairs(): + res[t].append(p) + for timeframe, inf_pairs in res.items(): + timerange_startup = deepcopy(self._timerange) + timerange_startup.subtract_start(timeframe_to_seconds( + timeframe) * self.strategy.startup_candle_count) + refresh_data( + datadir=self.config['datadir'], + pairs=inf_pairs, + exchange=self.exchange, + timeframe=timeframe, + timerange=timerange_startup, + data_format=self.config.get('dataformat_ohlcv', 'json'), + ) data = load_data( datadir=self.config['datadir'], @@ -125,8 +148,11 @@ class Edge: self._cached_pairs = {} logger.critical("No data found. Edge is stopped ...") return False - + # Fake run-mode to Edge + prior_rm = self.config['runmode'] + self.config['runmode'] = RunMode.EDGE preprocessed = self.strategy.ohlcvdata_to_dataframe(data) + self.config['runmode'] = prior_rm # Print timeframe min_date, max_date = get_timerange(preprocessed) From a710b7dc01296e2b3330da0cf845bfbf3f62f6e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 07:46:30 +0200 Subject: [PATCH 0390/1386] Update tests to match new behaviour --- tests/conftest.py | 2 ++ tests/edge/test_edge.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 83a34359a..ef2bd0613 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver +from freqtrade.state import RunMode from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, mock_trade_5, mock_trade_6) @@ -1677,6 +1678,7 @@ def buy_order_fee(): @pytest.fixture(scope="function") def edge_conf(default_conf): conf = deepcopy(default_conf) + conf['runmode'] = RunMode.DRY_RUN conf['max_open_trades'] = -1 conf['tradable_balance_ratio'] = 0.5 conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 25e0da5e2..c4620e1c7 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -344,6 +344,8 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): def test_edge_process_no_pairs(mocker, edge_conf, caplog): edge_conf['exchange']['pair_whitelist'] = [] + mocker.patch('freqtrade.freqtradebot.validate_config_consistency') + freqtrade = get_patched_freqtradebot(mocker, edge_conf) fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) mocker.patch('freqtrade.edge.edge_positioning.refresh_data') From 4f529fe4246bd339bed059619c1dd57e850d857b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 19:34:10 +0200 Subject: [PATCH 0391/1386] Don't use Arrow to get min/max backtest dates --- freqtrade/configuration/timerange.py | 7 ++++--- freqtrade/data/history/history_utils.py | 4 ++-- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/hyperopt.py | 6 +++--- freqtrade/optimize/optimize_reports.py | 13 ++++++------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 6072e296c..6979c8cd1 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -3,6 +3,7 @@ This module contains the argument manager class """ import logging import re +from datetime import datetime from typing import Optional import arrow @@ -43,7 +44,7 @@ class TimeRange: self.startts = self.startts - seconds def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int, - min_date: arrow.Arrow) -> None: + min_date: datetime) -> None: """ Adjust startts by candles. Applies only if no startup-candles have been available. @@ -54,11 +55,11 @@ class TimeRange: :return: None (Modifies the object in place) """ if (not self.starttype or (startup_candles - and min_date.int_timestamp >= self.startts)): + and min_date.timestamp() >= self.startts)): # If no startts was defined, or backtest-data starts at the defined backtest-date logger.warning("Moving start-date by %s candles to account for startup time.", startup_candles) - self.startts = (min_date.int_timestamp + timeframe_secs * startup_candles) + self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles) self.starttype = 'date' @staticmethod diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 58965abe0..32c7ce7f9 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -367,7 +367,7 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], logger.exception(f'Could not convert {pair} to OHLCV.') -def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: +def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]: """ Get the maximum common timerange for the given backtest data. @@ -375,7 +375,7 @@ def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] :return: tuple containing min_date, max_date """ timeranges = [ - (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) + (frame['date'].min().to_pydatetime(), frame['date'].max().to_pydatetime()) for frame in data.values() ] return (min(timeranges, key=operator.itemgetter(0))[0], diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 54e7b806f..76d76dd26 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -456,8 +456,8 @@ class Backtesting: # Execute backtest and store results results = self.backtest( processed=preprocessed, - start_date=min_date.datetime, - end_date=max_date.datetime, + start_date=min_date, + end_date=max_date, max_open_trades=max_open_trades, position_stacking=self.config.get('position_stacking', False), enable_protections=self.config.get('enable_protections', False), diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 639e9fb93..e0a6d50a0 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -273,8 +273,8 @@ class Hyperopt: bt_results = self.backtesting.backtest( processed=processed, - start_date=self.min_date.datetime, - end_date=self.max_date.datetime, + start_date=self.min_date, + end_date=self.max_date, max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, enable_protections=self.config.get('enable_protections', False), @@ -314,7 +314,7 @@ class Hyperopt: if trade_count >= self.config['hyperopt_min_trades']: loss = self.calculate_loss(results=backtesting_results['results'], trade_count=trade_count, - min_date=min_date.datetime, max_date=max_date.datetime, + min_date=min_date, max_date=max_date, config=self.config, processed=processed) return { 'loss': loss, diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 9a60c7c4b..170e19ecc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Union -from arrow import Arrow from numpy import int64 from pandas import DataFrame from tabulate import tabulate @@ -259,7 +258,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: def generate_strategy_stats(btdata: Dict[str, DataFrame], strategy: str, content: Dict[str, Any], - min_date: Arrow, max_date: Arrow, + min_date: datetime, max_date: datetime, market_change: float ) -> Dict[str, Any]: """ @@ -314,10 +313,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), - 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.int_timestamp * 1000, - 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.int_timestamp * 1000, + 'backtest_start': min_date, + 'backtest_start_ts': int(min_date.timestamp() * 1000), + 'backtest_end': max_date, + 'backtest_end_ts': int(max_date.timestamp() * 1000), 'backtest_days': backtest_days, 'backtest_run_start_ts': content['backtest_start_time'], @@ -397,7 +396,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], def generate_backtest_stats(btdata: Dict[str, DataFrame], all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], - min_date: Arrow, max_date: Arrow + min_date: datetime, max_date: datetime ) -> Dict[str, Any]: """ :param btdata: Backtest data From 554f5f14b6a42366026a043d48f3790a6d3cc570 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 20:49:48 +0200 Subject: [PATCH 0392/1386] Raise exception if no data is left --- freqtrade/optimize/backtesting.py | 10 ++++++---- tests/optimize/test_backtesting.py | 14 +++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 76d76dd26..899da03e4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -9,7 +9,7 @@ from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple -from pandas import DataFrame +from pandas import DataFrame, NaT from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.constants import DATETIME_PRINT_FORMAT @@ -159,7 +159,7 @@ class Backtesting: logger.info(f'Loading data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') + f'({(max_date - min_date).days} days).') # Adjust startts forward if not enough data is available timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), @@ -449,10 +449,12 @@ class Backtesting: preprocessed[pair] = trim_dataframe(df, timerange, startup_candles=self.required_startup) min_date, max_date = history.get_timerange(preprocessed) - + if min_date is NaT or max_date is NaT: + raise OperationalException( + "No data left after adjusting for startup candles. ") logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') + f'({(max_date - min_date).days} days).') # Execute backtest and store results results = self.backtest( processed=preprocessed, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5dfc9dbc3..2ebea564b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -366,7 +366,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: # check the logs, that will contain the backtest result exists = [ 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:59:00 (0 days)..' + 'up to 2017-11-14 22:59:00 (0 days).' ] for line in exists: assert log_has(line, caplog) @@ -791,9 +791,9 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...' ] @@ -864,9 +864,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategyLegacy', @@ -960,9 +960,9 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategyLegacy', From 513be11fd98f86bc2eeda74ca42251f9b3521969 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 20:23:11 +0200 Subject: [PATCH 0393/1386] Fix hyperopt output closes #4892 --- freqtrade/optimize/hyperopt_tools.py | 3 ++- tests/optimize/test_hyperopt.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index f655582c4..8d6098257 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -157,7 +157,8 @@ class HyperoptTools(): result = '{\n' for k, param in p.items(): - result += " " * indent + f'"{k}": {param},' + result += " " * indent + f'"{k}": ' + result += f'"{param}",' if isinstance(param, str) else f'{param},' if k in non_optimized: result += " # value loaded from strategy" result += "\n" diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 774dd35a4..16b647df9 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1156,3 +1156,17 @@ def test_SKDecimal(): assert space.transform([2.0]) == [200] assert space.transform([1.0]) == [100] assert space.transform([1.5, 1.6]) == [150, 160] + + +def test___pprint(): + params = {'buy_std': 1.2, 'buy_rsi': 31, 'buy_enable': True, 'buy_what': 'asdf'} + non_params = {'buy_notoptimied': 55} + + x = HyperoptTools._pprint(params, non_params) + assert x == """{ + "buy_std": 1.2, + "buy_rsi": 31, + "buy_enable": True, + "buy_what": "asdf", + "buy_notoptimied": 55, # value loaded from strategy +}""" From d34da3f981496068928d85f6378be6b10b6678b5 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 12:17:59 +0300 Subject: [PATCH 0394/1386] Revert "Add dataframe parameter to custom_stoploss() and custom_sell() methods." This reverts commit 595b8735f80df633834a4d8266694cdcb52287b8. # Conflicts: # freqtrade/optimize/backtesting.py # freqtrade/strategy/interface.py --- freqtrade/freqtradebot.py | 13 ++++++------- freqtrade/optimize/backtesting.py | 7 +++---- freqtrade/strategy/interface.py | 21 +++++++++------------ tests/strategy/test_default_strategy.py | 3 +-- tests/strategy/test_interface.py | 4 ++-- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c2b15d23f..09a5ea746 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,7 +11,6 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from pandas import DataFrame from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -784,10 +783,10 @@ class FreqtradeBot(LoggingMixin): config_ask_strategy = self.config.get('ask_strategy', {}) - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) @@ -814,13 +813,13 @@ class FreqtradeBot(LoggingMixin): # resulting in outdated RPC messages self._sell_rate_cache[trade.pair] = sell_rate - if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -951,13 +950,13 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float, + def _check_and_execute_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: """ Check and execute sell """ should_sell = self.strategy.should_sell( - dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell, + trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 899da03e4..80d816985 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -247,10 +247,9 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade, - sell_row: Tuple) -> Optional[LocalTrade]: + def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore + sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) @@ -398,7 +397,7 @@ class Backtesting: for trade in open_trades[pair]: # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(processed[pair], trade, row) + trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: # logger.debug(f"{pair} - Backtesting sell {trade}") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 63dcc75d9..c483e6afb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -277,7 +277,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: DataFrame, **kwargs) -> float: + current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -300,8 +300,7 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: DataFrame, - **kwargs) -> Optional[Union[str, bool]]: + current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ Custom sell signal logic indicating that specified position should be sold. Returning a string or True from this method is equal to setting sell signal on a candle at specified @@ -539,8 +538,8 @@ class IStrategy(ABC, HyperStrategyMixin): else: return False - def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime, - buy: bool, sell: bool, low: float = None, high: float = None, + def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, + sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell @@ -556,9 +555,8 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_min_max_rates(high or current_rate) - stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate, - trade=trade, current_time=date, - current_profit=current_profit, + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, + current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) # Set current rate to high for backtesting sell @@ -583,7 +581,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, - current_profit=current_profit, dataframe=dataframe) + current_profit=current_profit) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -620,7 +618,7 @@ class IStrategy(ABC, HyperStrategyMixin): # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE) - def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade, + def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, high: float = None) -> SellCheckTuple: """ @@ -638,8 +636,7 @@ class IStrategy(ABC, HyperStrategyMixin): )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, - current_profit=current_profit, - dataframe=dataframe) + current_profit=current_profit) # Sanity check - error cases will return None if stop_loss_value: # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index a8862e9c9..ec7b3c33d 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -41,5 +41,4 @@ def test_default_strategy(result, fee): rate=20000, time_in_force='gtc', sell_reason='roi') is True assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), - current_rate=20_000, current_profit=0.05, dataframe=None - ) == strategy.stoploss + current_rate=20_000, current_profit=0.05) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a241d7f43..182dde335 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -360,7 +360,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, current_time=now, current_profit=profit, - force_stoploss=0, high=None, dataframe=None) + force_stoploss=0, high=None) assert isinstance(sl_flag, SellCheckTuple) assert sl_flag.sell_type == expected if expected == SellType.NONE: @@ -371,7 +371,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, current_time=now, current_profit=profit2, - force_stoploss=0, high=None, dataframe=None) + force_stoploss=0, high=None) assert sl_flag.sell_type == expected2 if expected2 == SellType.NONE: assert sl_flag.sell_flag is False From dc6e702fecc0c7480b9c5291e290362d8527247b Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 12:20:25 +0300 Subject: [PATCH 0395/1386] Pass current_time to confirm_trade_entry/confirm_trade_exit. --- freqtrade/freqtradebot.py | 6 +++--- freqtrade/strategy/interface.py | 7 +++++-- tests/strategy/test_default_strategy.py | 6 ++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 09a5ea746..d2e6ed417 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -552,7 +552,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, - time_in_force=time_in_force): + time_in_force=time_in_force, current_time=datetime.utcnow()): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) @@ -1190,8 +1190,8 @@ class FreqtradeBot(LoggingMixin): if 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, - sell_reason=sell_reason.sell_reason): + time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, + current_time=datetime.utcnow()): logger.info(f"User requested abortion of selling {trade.pair}") return False diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c483e6afb..66adc36ec 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -229,7 +229,7 @@ class IStrategy(ABC, HyperStrategyMixin): pass def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, **kwargs) -> bool: + time_in_force: str, current_time: datetime, **kwargs) -> bool: """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or @@ -244,6 +244,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param amount: Amount in target (quote) 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 **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. False aborts the process @@ -251,7 +252,8 @@ class IStrategy(ABC, HyperStrategyMixin): return True def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: + rate: float, time_in_force: str, sell_reason: str, + current_time: datetime, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or @@ -270,6 +272,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param sell_reason: Sell reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] + :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 sell-order is placed on the exchange. False aborts the process diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index ec7b3c33d..92ac9f63a 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -36,9 +36,11 @@ def test_default_strategy(result, fee): ) assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, - rate=20000, time_in_force='gtc') is True + rate=20000, time_in_force='gtc', + current_time=datetime.utcnow()) is True assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, - rate=20000, time_in_force='gtc', sell_reason='roi') is True + rate=20000, time_in_force='gtc', sell_reason='roi', + current_time=datetime.utcnow()) is True assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), current_rate=20_000, current_profit=0.05) == strategy.stoploss From cdfa6adbe55af690c7e7eb007a53f8a566ec0f84 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 12:20:54 +0300 Subject: [PATCH 0396/1386] Store pair datafrmes in dataprovider for backtesting. --- freqtrade/data/dataprovider.py | 3 +++ freqtrade/optimize/backtesting.py | 16 +++++++++++----- tests/strategy/test_interface.py | 8 ++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b4dea0743..6cc32157e 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -173,3 +173,6 @@ class DataProvider: return self._pairlists.whitelist.copy() else: raise OperationalException("Dataprovider was not initialized with a pairlist provider.") + + def clear_cache(self): + self.__cached_pairs = {} diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 80d816985..73ad4e774 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -64,8 +64,8 @@ class Backtesting: self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - dataprovider = DataProvider(self.config, self.exchange) - IStrategy.dp = dataprovider + self.dataprovider = DataProvider(self.config, self.exchange) + IStrategy.dp = self.dataprovider if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): @@ -96,7 +96,7 @@ class Backtesting: "PrecisionFilter not allowed for backtesting multiple strategies." ) - dataprovider.add_pairlisthandler(self.pairlists) + self.dataprovider.add_pairlisthandler(self.pairlists) self.pairlists.refresh_pairlist() if len(self.pairlists.whitelist) == 0: @@ -176,6 +176,7 @@ class Backtesting: Trade.use_db = False PairLocks.reset_locks() Trade.reset_trades() + self.dataprovider.clear_cache() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ @@ -266,7 +267,8 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_reason): + sell_reason=sell.sell_reason, + current_time=sell_row[DATE_IDX].to_pydatetime()): return None trade.close(closerate, show_msg=False) @@ -286,7 +288,7 @@ class Backtesting: # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], - time_in_force=time_in_force): + time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): @@ -348,6 +350,10 @@ class Backtesting: trades: List[LocalTrade] = [] self.prepare_backtest(enable_protections) + # Update dataprovider cache + for pair, dataframe in processed.items(): + self.dataprovider._set_cached_df(pair, self.timeframe, dataframe) + # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) data: Dict = self._get_ohlcv_as_lists(processed) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 182dde335..ded396779 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -399,27 +399,27 @@ def test_custom_sell(default_conf, fee, caplog) -> None: ) now = arrow.utcnow().datetime - res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) assert res.sell_flag is False assert res.sell_type == SellType.NONE strategy.custom_sell = MagicMock(return_value=True) - res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) assert res.sell_flag is True assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_reason == 'custom_sell' strategy.custom_sell = MagicMock(return_value='hello world') - res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True assert res.sell_reason == 'hello world' caplog.clear() strategy.custom_sell = MagicMock(return_value='h' * 100) - res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True assert res.sell_reason == 'h' * 64 From 6af4de8fe86f66746a312144a9964cbbcc5ebb43 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 12:25:43 +0300 Subject: [PATCH 0397/1386] Remove dataframe parameter from docs. --- docs/strategy-advanced.md | 22 +++++++++------------- docs/strategy-customization.md | 3 +-- freqtrade/strategy/interface.py | 1 - 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index eb322df9d..383b2a1a9 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -60,7 +60,8 @@ from freqtrade.strategy import IStrategy, timeframe_to_prev_date class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, dataframe: DataFrame, **kwargs): + current_profit: float, **kwargs): + dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe) trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() @@ -105,8 +106,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -156,8 +156,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date_utc: @@ -183,8 +182,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -210,8 +208,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss @@ -250,8 +247,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: @@ -293,8 +289,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # Default return value result = 1 @@ -302,6 +297,7 @@ class AwesomeStrategy(IStrategy): # Using current_time directly would only work in backtesting. Live/dry runs need time to # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. + dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe) current_time = timeframe_to_prev_date(self.timeframe, current_time) current_row = dataframe.loc[dataframe['date'] == current_time].squeeze() if 'atr' in current_row: diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 59bfbde48..6c62c1e86 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -631,8 +631,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 66adc36ec..7483abf6d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -296,7 +296,6 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ From 6fb4d83ab3f2cb7b4402223b073ccb0a868fcb76 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 16:35:06 +0300 Subject: [PATCH 0398/1386] Fix dataprovider in hyperopt. --- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/hyperopt.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 73ad4e774..6d8b329bb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -353,6 +353,7 @@ class Backtesting: # Update dataprovider cache for pair, dataframe in processed.items(): self.dataprovider._set_cached_df(pair, self.timeframe, dataframe) + self.strategy.dp = self.dataprovider # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e0a6d50a0..5e3d01047 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -372,8 +372,6 @@ class Hyperopt: self.backtesting.exchange._api_async = None # type: ignore # self.backtesting.exchange = None # type: ignore self.backtesting.pairlists = None # type: ignore - self.backtesting.strategy.dp = None # type: ignore - IStrategy.dp = None # type: ignore cpus = cpu_count() logger.info(f"Found {cpus} CPU cores. Let's make them scream!") From 9b4f6b41a2ade4d1857fa0a6ed223967a965b26f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Mon, 3 May 2021 09:18:38 +0300 Subject: [PATCH 0399/1386] Use correct datetime. --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d2e6ed417..b3379a462 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -552,7 +552,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, - time_in_force=time_in_force, current_time=datetime.utcnow()): + time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) @@ -1191,7 +1191,7 @@ class FreqtradeBot(LoggingMixin): if 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, sell_reason=sell_reason.sell_reason, - current_time=datetime.utcnow()): + current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of selling {trade.pair}") return False From d344194b360bd453c4c6ea673a2724ef72b0e8ad Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Mon, 3 May 2021 09:47:58 +0300 Subject: [PATCH 0400/1386] Fix dataprovider in hyperopt. --- freqtrade/data/dataprovider.py | 141 +++++++++++++++++------------- freqtrade/optimize/backtesting.py | 4 +- 2 files changed, 79 insertions(+), 66 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 6cc32157e..731815572 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -45,40 +45,6 @@ class DataProvider: """ self._pairlists = pairlists - def refresh(self, - pairlist: ListPairsWithTimeframes, - helping_pairs: ListPairsWithTimeframes = None) -> None: - """ - Refresh data, called with each cycle - """ - if helping_pairs: - self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) - else: - self._exchange.refresh_latest_ohlcv(pairlist) - - @property - def available_pairs(self) -> ListPairsWithTimeframes: - """ - Return a list of tuples containing (pair, timeframe) for which data is currently cached. - Should be whitelist + open trades. - """ - return list(self._exchange._klines.keys()) - - def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: - """ - Get candle (OHLCV) data for the given pair as DataFrame - Please use the `available_pairs` method to verify which pairs are currently cached. - :param pair: pair to get the data for - :param timeframe: Timeframe to get data for - :param copy: copy dataframe before returning if True. - Use False only for read-only operations (where the dataframe is not modified) - """ - if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - return self._exchange.klines((pair, timeframe or self._config['timeframe']), - copy=copy) - else: - return DataFrame() - def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame: """ Get stored historical candle (OHLCV) data @@ -123,35 +89,6 @@ class DataProvider: return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) - def market(self, pair: str) -> Optional[Dict[str, Any]]: - """ - Return market data for the pair - :param pair: Pair to get the data for - :return: Market data dict from ccxt or None if market info is not available for the pair - """ - return self._exchange.markets.get(pair) - - def ticker(self, pair: str): - """ - Return last ticker data from exchange - :param pair: Pair to get the data for - :return: Ticker dict from exchange or empty dict if ticker is not available for the pair - """ - try: - return self._exchange.fetch_ticker(pair) - except ExchangeError: - return {} - - def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: - """ - Fetch latest l2 orderbook data - Warning: Does a network request - so use with common sense. - :param pair: pair to get the data for - :param maximum: Maximum number of orderbook entries to query - :return: dict including bids/asks with a total of `maximum` entries. - """ - return self._exchange.fetch_l2_order_book(pair, maximum) - @property def runmode(self) -> RunMode: """ @@ -175,4 +112,82 @@ class DataProvider: raise OperationalException("Dataprovider was not initialized with a pairlist provider.") def clear_cache(self): + """ + Clear pair dataframe cache. + """ self.__cached_pairs = {} + + # Exchange functions + + def refresh(self, + pairlist: ListPairsWithTimeframes, + helping_pairs: ListPairsWithTimeframes = None) -> None: + """ + Refresh data, called with each cycle + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + if helping_pairs: + self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) + else: + self._exchange.refresh_latest_ohlcv(pairlist) + + @property + def available_pairs(self) -> ListPairsWithTimeframes: + """ + Return a list of tuples containing (pair, timeframe) for which data is currently cached. + Should be whitelist + open trades. + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + return list(self._exchange._klines.keys()) + + def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: + """ + Get candle (OHLCV) data for the given pair as DataFrame + Please use the `available_pairs` method to verify which pairs are currently cached. + :param pair: pair to get the data for + :param timeframe: Timeframe to get data for + :param copy: copy dataframe before returning if True. + Use False only for read-only operations (where the dataframe is not modified) + """ + if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + return self._exchange.klines((pair, timeframe or self._config['timeframe']), + copy=copy) + else: + return DataFrame() + + def market(self, pair: str) -> Optional[Dict[str, Any]]: + """ + Return market data for the pair + :param pair: Pair to get the data for + :return: Market data dict from ccxt or None if market info is not available for the pair + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + return self._exchange.markets.get(pair) + + def ticker(self, pair: str): + """ + Return last ticker data from exchange + :param pair: Pair to get the data for + :return: Ticker dict from exchange or empty dict if ticker is not available for the pair + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + try: + return self._exchange.fetch_ticker(pair) + except ExchangeError: + return {} + + def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: + """ + Fetch latest l2 orderbook data + Warning: Does a network request - so use with common sense. + :param pair: pair to get the data for + :param maximum: Maximum number of orderbook entries to query + :return: dict including bids/asks with a total of `maximum` entries. + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + return self._exchange.fetch_l2_order_book(pair, maximum) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6d8b329bb..d3e0afe7b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -63,9 +63,7 @@ class Backtesting: self.all_results: Dict[str, Dict] = {} self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - self.dataprovider = DataProvider(self.config, self.exchange) - IStrategy.dp = self.dataprovider if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): @@ -132,6 +130,7 @@ class Backtesting: Load strategy into backtesting """ self.strategy: IStrategy = strategy + strategy.dp = self.dataprovider # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case @@ -353,7 +352,6 @@ class Backtesting: # Update dataprovider cache for pair, dataframe in processed.items(): self.dataprovider._set_cached_df(pair, self.timeframe, dataframe) - self.strategy.dp = self.dataprovider # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) From 4b6cd69c81e1a3442941ec03c57f26260e57812d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 20:08:31 +0200 Subject: [PATCH 0401/1386] Add test for no-exchange dataprovider --- freqtrade/data/dataprovider.py | 14 +++++++++----- freqtrade/optimize/hyperopt.py | 1 - tests/data/test_dataprovider.py | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 731815572..aad50e404 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -19,6 +19,8 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) +NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.' + class DataProvider: @@ -126,7 +128,7 @@ class DataProvider: Refresh data, called with each cycle """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) if helping_pairs: self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) else: @@ -139,7 +141,7 @@ class DataProvider: Should be whitelist + open trades. """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) return list(self._exchange._klines.keys()) def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: @@ -151,6 +153,8 @@ class DataProvider: :param copy: copy dataframe before returning if True. Use False only for read-only operations (where the dataframe is not modified) """ + if self._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): return self._exchange.klines((pair, timeframe or self._config['timeframe']), copy=copy) @@ -164,7 +168,7 @@ class DataProvider: :return: Market data dict from ccxt or None if market info is not available for the pair """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.markets.get(pair) def ticker(self, pair: str): @@ -174,7 +178,7 @@ class DataProvider: :return: Ticker dict from exchange or empty dict if ticker is not available for the pair """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) try: return self._exchange.fetch_ticker(pair) except ExchangeError: @@ -189,5 +193,5 @@ class DataProvider: :return: dict including bids/asks with a total of `maximum` entries. """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.fetch_l2_order_book(pair, maximum) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 5e3d01047..5ccf02d01 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -31,7 +31,6 @@ from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver -from freqtrade.strategy import IStrategy # Suppress scikit-learn FutureWarnings from skopt diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 6b33fa7f2..c3b210d9d 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -246,3 +246,24 @@ def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history): assert dataframe.empty assert isinstance(time, datetime) assert time == datetime(1970, 1, 1, tzinfo=timezone.utc) + + +def test_no_exchange_mode(default_conf): + dp = DataProvider(default_conf, None) + + message = "Exchange is not available to DataProvider." + + with pytest.raises(OperationalException, match=message): + dp.refresh([()]) + + with pytest.raises(OperationalException, match=message): + dp.ohlcv('XRP/USDT', '5m') + + with pytest.raises(OperationalException, match=message): + dp.market('XRP/USDT') + + with pytest.raises(OperationalException, match=message): + dp.ticker('XRP/USDT') + + with pytest.raises(OperationalException, match=message): + dp.orderbook('XRP/USDT', 20) From 1b01ad6f8588d4bd145f05ec6137b30e60b6b352 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 7 May 2021 17:27:48 +0300 Subject: [PATCH 0402/1386] Make exchange parameter optional and do not use it as parameter in backtesting. --- freqtrade/data/dataprovider.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index aad50e404..727768ebb 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -24,7 +24,7 @@ NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.' class DataProvider: - def __init__(self, config: dict, exchange: Exchange, pairlists=None) -> None: + def __init__(self, config: dict, exchange: Optional[Exchange], pairlists=None) -> None: self._config = config self._exchange = exchange self._pairlists = pairlists diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d3e0afe7b..150baa9bc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -63,7 +63,7 @@ class Backtesting: self.all_results: Dict[str, Dict] = {} self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - self.dataprovider = DataProvider(self.config, self.exchange) + self.dataprovider = DataProvider(self.config, None) if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): From f1eb6535452fb1598b9999b59ae2e5167ce074f7 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 7 May 2021 17:28:06 +0300 Subject: [PATCH 0403/1386] Fix strategy protections not being loaded in backtesting. --- freqtrade/optimize/backtesting.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 150baa9bc..80eb34c30 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -110,8 +110,6 @@ class Backtesting: PairLocks.timeframe = self.config['timeframe'] PairLocks.use_db = False PairLocks.reset_locks() - if self.config.get('enable_protections', False): - self.protections = ProtectionManager(self.config) self.wallets = Wallets(self.config, self.exchange, log=False) @@ -135,6 +133,12 @@ class Backtesting: # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + if self.config.get('enable_protections', False): + conf = self.config + if hasattr(strategy, 'protections'): + conf = deepcopy(conf) + conf['protections'] = strategy.protections + self.protections = ProtectionManager(conf) def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]: """ From 70189b19920bfaf705196fa3e0da72d89967ac43 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 May 2021 17:24:41 +0200 Subject: [PATCH 0404/1386] Move dockerfile and document M1 image existance --- .../Dockerfile.aarch64 | 0 docs/docker_quickstart.md | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+) rename Dockerfile.aarch64 => docker/Dockerfile.aarch64 (100%) diff --git a/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 similarity index 100% rename from Dockerfile.aarch64 rename to docker/Dockerfile.aarch64 diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 5f48782d2..8d8582609 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -48,6 +48,8 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Download the docker-compose file from the repository curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml + # Edit the compose file to use an image named `*_pi` (stable_pi or develop_pi) + # Pull the freqtrade image docker-compose pull @@ -65,6 +67,30 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # image: freqtradeorg/freqtrade:develop_pi ``` +=== "ARM64 (Mac M1)" + Make sure that your docker installation is running in native mode + + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml + + # Edit the compose file, uncomment the "build" step and use "./docker/Dockerfile.aarch64" + # Also, change the image name to something of your liking + + # Build the freqtrade image (this may take a while) + docker-compose build + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + !!! Warning + You should not use the default image name - this can result in conflicting names between local and dockerhub and should therefore be avoided. + The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. From 8d8c782bd039780f8563e8edf7f735501c9013ab Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 8 May 2021 16:06:19 +0300 Subject: [PATCH 0405/1386] Slice dataframe in backtesting, preventing access to rows past current time. --- freqtrade/data/dataprovider.py | 21 ++++++++++++++++++--- freqtrade/optimize/backtesting.py | 8 ++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 727768ebb..a5d059e4a 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -20,6 +20,7 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.' +MAX_DATAFRAME_CANDLES = 1000 class DataProvider: @@ -29,6 +30,14 @@ class DataProvider: self._exchange = exchange self._pairlists = pairlists self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} + self.__slice_index = None + + def _set_dataframe_max_index(self, limit_index: int): + """ + Limit analyzed dataframe to max specified index. + :param limit_index: dataframe index. + """ + self.__slice_index = limit_index def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: """ @@ -85,10 +94,16 @@ class DataProvider: combination. Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached. """ - if (pair, timeframe) in self.__cached_pairs: - return self.__cached_pairs[(pair, timeframe)] + pair_key = (pair, timeframe) + if pair_key in self.__cached_pairs: + if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + df, date = self.__cached_pairs[pair_key] + else: + max_index = self.__slice_index + df, date = self.__cached_pairs[pair_key] + df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index] + return df, date else: - return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) @property diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 80eb34c30..f7c984047 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -373,8 +373,9 @@ class Backtesting: open_trade_count_start = open_trade_count for i, pair in enumerate(data): + row_index = indexes[pair] try: - row = data[pair][indexes[pair]] + row = data[pair][row_index] except IndexError: # missing Data for one pair at the end. # Warnings for this are shown during data loading @@ -383,7 +384,10 @@ class Backtesting: # Waits until the time-counter reaches the start of the data for this pair. if row[DATE_IDX] > tmp: continue - indexes[pair] += 1 + + row_index += 1 + self.dataprovider._set_dataframe_max_index(row_index) # noqa + indexes[pair] = row_index # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected From 17b9e898d2a234b9c1861192af21618ca63442a1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 8 May 2021 16:56:59 +0300 Subject: [PATCH 0406/1386] Update docs displaying how to get last available and trade-open candles. --- docs/strategy-advanced.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 383b2a1a9..ceadf0ab0 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -265,12 +265,6 @@ class AwesomeStrategy(IStrategy): Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" -!!! Warning - Only use `dataframe` values up until and including `current_time` value. Reading past - `current_time` you will look into the future, which will produce incorrect backtesting results - and throw an exception in dry/live runs. - see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. - !!! Note `dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and `trade.open_date_utc` will not match the candle date precisely and using them directly will throw @@ -290,7 +284,6 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # Default return value result = 1 if trade: @@ -298,11 +291,19 @@ class AwesomeStrategy(IStrategy): # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe) - current_time = timeframe_to_prev_date(self.timeframe, current_time) - current_row = dataframe.loc[dataframe['date'] == current_time].squeeze() - if 'atr' in current_row: + current_candle = dataframe.loc[-1].squeeze() + if 'atr' in current_candle: # new stoploss relative to current_rate - new_stoploss = (current_rate - current_row['atr']) / current_rate + new_stoploss = (current_rate - current_candle['atr']) / current_rate + + # Round trade date to it's candle time. + trade_date = timeframe_to_prev_date(trade.open_date_utc) + trade_candle = dataframe.loc[dataframe['date'] == trade_date] + # Just opened trades do not have their candle complete yet therefore trade_candle may be None + if trade_candle is not None: + trade_candle = trade_candle.squeeze() + trade_stoploss = (current_rate - trade_candle['atr']) / current_rate + new_stoploss = max(new_stoploss, trade_stoploss) # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 From 2157923aeedb9c7e5d8ab386812843e99aa43e19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 May 2021 19:43:31 +0200 Subject: [PATCH 0407/1386] have edge send multiple messages if necessary closes #4519 --- freqtrade/edge/edge_positioning.py | 2 +- freqtrade/rpc/telegram.py | 14 +++++++++++--- tests/rpc/test_rpc_telegram.py | 9 +++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 721e22262..0449d6ebe 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -240,7 +240,7 @@ class Edge: return self._final_pairs - def accepted_pairs(self) -> list: + def accepted_pairs(self) -> List[Dict[str, Any]]: """ return a list of accepted pairs along with their winrate, expectancy and stoploss """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 97a01fc53..c619559de 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -861,9 +861,17 @@ class Telegram(RPCHandler): """ try: edge_pairs = self._rpc._rpc_edge() - edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') - message = f'Edge only validated following pairs:\n
{edge_pairs_tab}
' - self._send_msg(message, parse_mode=ParseMode.HTML) + if not edge_pairs: + message = 'Edge only validated following pairs:' + self._send_msg(message, parse_mode=ParseMode.HTML) + + for chunk in chunks(edge_pairs, 25): + edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple') + message = (f'Edge only validated following pairs:\n' + f'
{edge_pairs_tab}
') + + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6a36c12a7..6d42a6845 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1102,6 +1102,15 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
     assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
 
+    msg_mock.reset_mock()
+
+    mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
+        return_value={}))
+    telegram._edge(update=update, context=MagicMock())
+    assert msg_mock.call_count == 1
+    assert 'Edge only validated following pairs:' in msg_mock.call_args_list[0][0][0]
+    assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
+
 
 def test_telegram_trades(mocker, update, default_conf, fee):
 

From 92186d89a2fcbbb03eaf789ccf52238b9b934936 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 9 May 2021 09:56:36 +0200
Subject: [PATCH 0408/1386] Add some changes to strategytemplate

---
 freqtrade/data/dataprovider.py                           | 9 ++++++---
 freqtrade/optimize/backtesting.py                        | 2 +-
 .../templates/subtemplates/strategy_methods_advanced.j2  | 7 +++++--
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py
index a5d059e4a..1a86eece5 100644
--- a/freqtrade/data/dataprovider.py
+++ b/freqtrade/data/dataprovider.py
@@ -30,7 +30,7 @@ class DataProvider:
         self._exchange = exchange
         self._pairlists = pairlists
         self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
-        self.__slice_index = None
+        self.__slice_index: Optional[int] = None
 
     def _set_dataframe_max_index(self, limit_index: int):
         """
@@ -88,6 +88,8 @@ class DataProvider:
 
     def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
         """
+        Retrieve the analyzed dataframe. Returns the full dataframe in trade mode (live / dry),
+        and the last 1000 candles (up to the time evaluated at this moment) in all other modes.
         :param pair: pair to get the data for
         :param timeframe: timeframe to get data for
         :return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe
@@ -99,9 +101,10 @@ class DataProvider:
             if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
                 df, date = self.__cached_pairs[pair_key]
             else:
-                max_index = self.__slice_index
                 df, date = self.__cached_pairs[pair_key]
-                df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index]
+                if self.__slice_index is not None:
+                    max_index = self.__slice_index
+                    df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index]
             return df, date
         else:
             return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index f7c984047..e057d8189 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -386,7 +386,7 @@ class Backtesting:
                     continue
 
                 row_index += 1
-                self.dataprovider._set_dataframe_max_index(row_index)   # noqa
+                self.dataprovider._set_dataframe_max_index(row_index)
                 indexes[pair] = row_index
 
                 # without positionstacking, we can only have one open trade per pair.
diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
index c69b52cad..2a9ac0690 100644
--- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
+++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
@@ -39,7 +39,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
     return self.stoploss
 
 def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
-                        time_in_force: str, **kwargs) -> bool:
+                        time_in_force: str, current_time: 'datetime', **kwargs) -> bool:
     """
     Called right before placing a buy order.
     Timing for this function is critical, so avoid doing heavy computations or
@@ -54,6 +54,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
     :param amount: Amount in target (quote) 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 **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.
         False aborts the process
@@ -61,7 +62,8 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
     return True
 
 def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
-                       rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
+                       rate: float, time_in_force: str, sell_reason: str,
+                       current_time: 'datetime', **kwargs) -> bool:
     """
     Called right before placing a regular sell order.
     Timing for this function is critical, so avoid doing heavy computations or
@@ -80,6 +82,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
     :param sell_reason: Sell reason.
         Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
                         'sell_signal', 'force_sell', 'emergency_sell']
+    :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 sell-order is placed on the exchange.
         False aborts the process

From 00e93dad024c2e1be045d4d66cbba3d52928e081 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 9 May 2021 10:04:56 +0200
Subject: [PATCH 0409/1386] Fix mistake in the docs

---
 docs/strategy-advanced.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index ceadf0ab0..e52beec3b 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -61,7 +61,7 @@ from freqtrade.strategy import IStrategy, timeframe_to_prev_date
 class AwesomeStrategy(IStrategy):
     def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                     current_profit: float, **kwargs):
-        dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
         trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
         trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
 
@@ -290,7 +290,7 @@ class AwesomeStrategy(IStrategy):
             # Using current_time directly would only work in backtesting. Live/dry runs need time to
             # be rounded to previous candle to be used as dataframe index. Rounding must also be 
             # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing.
-            dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
             current_candle = dataframe.loc[-1].squeeze()
             if 'atr' in current_candle:
                 # new stoploss relative to current_rate

From 0a0e7ce5f502cdf1e9797daed7d162167075a2c2 Mon Sep 17 00:00:00 2001
From: Boris Pruessmann 
Date: Sun, 9 May 2021 14:28:54 +0200
Subject: [PATCH 0410/1386] Documentation for running arm64 builds

---
 docs/docker_quickstart.md | 24 ++++++++++++++----------
 1 file changed, 14 insertions(+), 10 deletions(-)

diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md
index 8d8582609..ce6d0b503 100644
--- a/docs/docker_quickstart.md
+++ b/docs/docker_quickstart.md
@@ -67,29 +67,33 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
         # image: freqtradeorg/freqtrade:develop_pi
         ```
 
-=== "ARM64 (Mac M1)"
-    Make sure that your docker installation is running in native mode
+=== "ARM 64 Systenms (Jetson Nano, Mac M1, Raspberry Pi 4 8GB)"
+    In case of a Mac M1, make sure that your docker installation is running in native mode
 
     ``` bash
     mkdir ft_userdata
     cd ft_userdata/
+
+    # arm64 images are not yer provided via Docker Hub and need to be build locally first. Depending on the device,
+    # this may take a few minutes (Apple M1) or up to two hours (Raspberry Pi)
+    git clone  https://github.com/freqtrade/freqtrade.git
+    docker build -f ./freqtrade/docker/Dockerfile.aarch64 -t freqtradeorg/freqtrade:develop_arm64 freqtrade
+
     # Download the docker-compose file from the repository
     curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
 
-    # Edit the compose file, uncomment the "build" step and use "./docker/Dockerfile.aarch64"
-    # Also, change the image name to something of your liking
-
-    # Build the freqtrade image (this may take a while)
-    docker-compose build
-
     # Create user directory structure
     docker-compose run --rm freqtrade create-userdir --userdir user_data
 
     # Create configuration - Requires answering interactive questions
     docker-compose run --rm freqtrade new-config --config user_data/config.json
     ```
-    !!! Warning
-        You should not use the default image name - this can result in conflicting names between local and dockerhub and should therefore be avoided.
+
+    !!! Note "Change your docker Image"
+        You have to change the docker image in the docker-compose file for your arm64 build to work properly.
+        ``` yml
+        image: freqtradeorg/freqtrade:develop_arm64
+        ```
 
 The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.
 The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections.

From f2add44253f62fe67f10d5cb3c30dd10b649f82c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Boris=20Pr=C3=BC=C3=9Fmann?=
 
Date: Sun, 9 May 2021 17:27:30 +0200
Subject: [PATCH 0411/1386] Update docs/docker_quickstart.md

Co-authored-by: Matthias 
---
 docs/docker_quickstart.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md
index ce6d0b503..51386a4e3 100644
--- a/docs/docker_quickstart.md
+++ b/docs/docker_quickstart.md
@@ -74,7 +74,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
     mkdir ft_userdata
     cd ft_userdata/
 
-    # arm64 images are not yer provided via Docker Hub and need to be build locally first. Depending on the device,
+    # arm64 images are not yet provided via Docker Hub and need to be build locally first. Depending on the device,
     # this may take a few minutes (Apple M1) or up to two hours (Raspberry Pi)
     git clone  https://github.com/freqtrade/freqtrade.git
     docker build -f ./freqtrade/docker/Dockerfile.aarch64 -t freqtradeorg/freqtrade:develop_arm64 freqtrade

From 1c408c04041830cac86969bec0d026afa9717870 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 9 May 2021 19:47:37 +0200
Subject: [PATCH 0412/1386] Add small tests for backtest mode

---
 tests/data/test_dataprovider.py | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py
index c3b210d9d..b87258c64 100644
--- a/tests/data/test_dataprovider.py
+++ b/tests/data/test_dataprovider.py
@@ -247,6 +247,25 @@ def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history):
     assert isinstance(time, datetime)
     assert time == datetime(1970, 1, 1, tzinfo=timezone.utc)
 
+    # Test backtest mode
+    default_conf["runmode"] = RunMode.BACKTEST
+    dp._set_dataframe_max_index(1)
+    dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
+
+    assert len(dataframe) == 1
+
+    dp._set_dataframe_max_index(2)
+    dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
+    assert len(dataframe) == 2
+
+    dp._set_dataframe_max_index(3)
+    dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
+    assert len(dataframe) == 3
+
+    dp._set_dataframe_max_index(500)
+    dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
+    assert len(dataframe) == len(ohlcv_history)
+
 
 def test_no_exchange_mode(default_conf):
     dp = DataProvider(default_conf, None)
@@ -267,3 +286,6 @@ def test_no_exchange_mode(default_conf):
 
     with pytest.raises(OperationalException, match=message):
         dp.orderbook('XRP/USDT', 20)
+
+    with pytest.raises(OperationalException, match=message):
+        dp.available_pairs()

From d495ea36932011babb3dcafeaa1795a5e986f71c Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 9 May 2021 19:53:41 +0200
Subject: [PATCH 0413/1386] Update docs about availability of get_analyzed

---
 docs/strategy-customization.md | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md
index 6c62c1e86..49456c047 100644
--- a/docs/strategy-customization.md
+++ b/docs/strategy-customization.md
@@ -422,10 +422,6 @@ if self.dp:
     Returns an empty dataframe if the requested pair was not cached.
     This should not happen when using whitelisted pairs.
 
-
-!!! Warning "Warning about backtesting"
-    This method will return an empty dataframe during backtesting.
-
 ### *orderbook(pair, maximum)*
 
 ``` python

From a7bd051f6b2aac583a7448d8fe514fea817eb3b2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:26:26 +0000
Subject: [PATCH 0414/1386] Bump mkdocs-material from 7.1.3 to 7.1.4

Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.3 to 7.1.4.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.3...7.1.4)

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 a431b1702..88f336cb1 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,3 +1,3 @@
-mkdocs-material==7.1.3
+mkdocs-material==7.1.4
 mdx_truly_sane_lists==1.2
 pymdown-extensions==8.1.1

From a7cd8fc578372cae26fc257c6cfcd10998d84eb4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:26:36 +0000
Subject: [PATCH 0415/1386] Bump sqlalchemy from 1.4.12 to 1.4.14

Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.12 to 1.4.14.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 49e1758df..f0676fdea 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,7 @@ ccxt==1.49.30
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==3.4.7
 aiohttp==3.7.4.post0
-SQLAlchemy==1.4.12
+SQLAlchemy==1.4.14
 python-telegram-bot==13.5
 arrow==1.1.0
 cachetools==4.2.2

From 0a82e2b06143d37fde8ba002800f0d7c9a16907b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:26:52 +0000
Subject: [PATCH 0416/1386] Bump pytest from 6.2.3 to 6.2.4

Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.3 to 6.2.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.3...6.2.4)

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 adf5a81c3..2b2314f85 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -8,7 +8,7 @@ flake8==3.9.1
 flake8-type-annotations==0.1.0
 flake8-tidy-imports==4.2.1
 mypy==0.812
-pytest==6.2.3
+pytest==6.2.4
 pytest-asyncio==0.15.1
 pytest-cov==2.11.1
 pytest-mock==3.6.0

From 6eb47b0aebf786ed533b991d56a586f135ffc3da Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:27:03 +0000
Subject: [PATCH 0417/1386] Bump flake8 from 3.9.1 to 3.9.2

Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.1 to 3.9.2.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.1...3.9.2)

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 adf5a81c3..ca297371f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,7 +4,7 @@
 -r requirements-hyperopt.txt
 
 coveralls==3.0.1
-flake8==3.9.1
+flake8==3.9.2
 flake8-type-annotations==0.1.0
 flake8-tidy-imports==4.2.1
 mypy==0.812

From 5ecd86ed5694721fedddd93fd923311593b302e5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:27:11 +0000
Subject: [PATCH 0418/1386] Bump pymdown-extensions from 8.1.1 to 8.2

Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 8.1.1 to 8.2.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/8.1.1...8.2)

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 a431b1702..70b33287e 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,3 +1,3 @@
 mkdocs-material==7.1.3
 mdx_truly_sane_lists==1.2
-pymdown-extensions==8.1.1
+pymdown-extensions==8.2

From 93268ba16d5aba78518ca6ce2f21227f4e6c7a63 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:27:22 +0000
Subject: [PATCH 0419/1386] Bump pytest-mock from 3.6.0 to 3.6.1

Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.6.0 to 3.6.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.6.0...v3.6.1)

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 adf5a81c3..dfb28dc05 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -11,7 +11,7 @@ mypy==0.812
 pytest==6.2.3
 pytest-asyncio==0.15.1
 pytest-cov==2.11.1
-pytest-mock==3.6.0
+pytest-mock==3.6.1
 pytest-random-order==1.0.4
 isort==5.8.0
 

From 43c7382d24aba1381dde7776fc67f724f61316a5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:27:36 +0000
Subject: [PATCH 0420/1386] Bump fastapi from 0.63.0 to 0.64.0

Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.63.0 to 0.64.0.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.63.0...0.64.0)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 49e1758df..7d72a63da 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -31,7 +31,7 @@ python-rapidjson==1.0
 sdnotify==0.3.2
 
 # API Server
-fastapi==0.63.0
+fastapi==0.64.0
 uvicorn==0.13.4
 pyjwt==2.1.0
 aiofiles==0.6.0

From 8e6a95e11b57c4060955bd4091972de40e766a50 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 10:50:05 +0000
Subject: [PATCH 0421/1386] Bump ccxt from 1.49.30 to 1.49.73

Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.49.30 to 1.49.73.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
- [Commits](https://github.com/ccxt/ccxt/compare/1.49.30...1.49.73)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 281bf4574..3c50fa586 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 numpy==1.20.2
 pandas==1.2.4
 
-ccxt==1.49.30
+ccxt==1.49.73
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==3.4.7
 aiohttp==3.7.4.post0

From 3d6b3f1d6aa39fe46b8d684d8be76d8986866293 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Mon, 10 May 2021 15:08:28 +0200
Subject: [PATCH 0422/1386] Add Issue config.yml

---
 .github/ISSUE_TEMPLATE/config.yml | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 .github/ISSUE_TEMPLATE/config.yml

diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..7591c1fb0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,6 @@
+---
+blank_issues_enabled: false
+contact_links:
+  - name: Discord Server
+    url: https://discord.gg/MA9v74M
+    about: Ask a question or get community support from our Discord server

From 425d97719abbf04747508e1c7d1f4e8b62578451 Mon Sep 17 00:00:00 2001
From: Robert Davey 
Date: Mon, 10 May 2021 19:42:37 +0100
Subject: [PATCH 0423/1386] Update strategy-advanced.md

Update custom_sell() example to comment that the current trade row is at trade open as written. Change "abstain" to something clearer for non-fluent English speakers.
---
 docs/strategy-advanced.md | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index e52beec3b..7285b24af 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -48,7 +48,7 @@ It is possible to define custom sell signals, indicating that specified position
 
 For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
 
-You should abstain from using custom_sell() signals in place of stoplosses though. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
+Using custom_sell() signals in place of stoplosses though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
 
 !!! Note
     Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
@@ -62,17 +62,19 @@ class AwesomeStrategy(IStrategy):
     def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                     current_profit: float, **kwargs):
         dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+        
+        # Get the row at trade open
         trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
-        trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
+        trade_open_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
 
         # Above 20% profit, sell when rsi < 80
         if current_profit > 0.2:
-            if trade_row['rsi'] < 80:
+            if trade_open_row['rsi'] < 80:
                 return 'rsi_below_80'
 
         # Between 2% and 10%, sell if EMA-long above EMA-short
         if 0.02 < current_profit < 0.1:
-            if trade_row['emalong'] > trade_row['emashort']:
+            if trade_open_row['emalong'] > trade_open_row['emashort']:
                 return 'ema_long_below_80'
 
         # Sell any positions at a loss if they are held for more than one day.

From bcab44560ab3b49edade5cb5ed7c9fcc72e47fc4 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 11 May 2021 06:23:21 +0200
Subject: [PATCH 0424/1386] Fix doc typo

---
 docs/strategy-advanced.md | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index 7285b24af..ada7a99be 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -293,7 +293,7 @@ class AwesomeStrategy(IStrategy):
             # be rounded to previous candle to be used as dataframe index. Rounding must also be 
             # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing.
             dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
-            current_candle = dataframe.loc[-1].squeeze()
+            current_candle = dataframe.iloc[-1].squeeze()
             if 'atr' in current_candle:
                 # new stoploss relative to current_rate
                 new_stoploss = (current_rate - current_candle['atr']) / current_rate
@@ -312,6 +312,11 @@ class AwesomeStrategy(IStrategy):
         return result
 ```
 
+!!! Warning "Using .iloc[-1]"
+    You can use `.iloc[-1]` here because `get_analyzed_dataframe()` only returns candles that backtesting is allowed to see.
+    This will not work in `populate_*` methods, so make sure to not use `.iloc[]` in that area.
+    Also, this will only work starting with version 2021.5.
+
 ---
 
 ## Custom order timeout rules

From e53bbec285a9056e09942bec92dc719ba98ec99e Mon Sep 17 00:00:00 2001
From: Kamontat Chantrachirathumrong 
Date: Wed, 12 May 2021 00:13:13 +0700
Subject: [PATCH 0425/1386] remove duplicate python3-pip

---
 docs/installation.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/installation.md b/docs/installation.md
index d2661f88f..18f5f4d68 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
     sudo apt-get update
 
     # install packages
-    sudo apt install -y python3-pip python3-venv python3-pandas python3-pip git
+    sudo apt install -y python3-pip python3-venv python3-pandas git
     ```
 
 === "RaspberryPi/Raspbian"

From 7398ea88e0008787add66c83978040c6c1fa89af Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 11 May 2021 20:30:37 +0200
Subject: [PATCH 0426/1386] Change optimize_reports to convert dates to string
 earlier

---
 freqtrade/optimize/optimize_reports.py  | 16 ++++++++--------
 tests/optimize/test_optimize_reports.py | 10 +++++-----
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py
index 170e19ecc..2be3c3e62 100644
--- a/freqtrade/optimize/optimize_reports.py
+++ b/freqtrade/optimize/optimize_reports.py
@@ -313,9 +313,9 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
         'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0,
         'profit_total': results['profit_abs'].sum() / starting_balance,
         'profit_total_abs': results['profit_abs'].sum(),
-        'backtest_start': min_date,
+        'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
         'backtest_start_ts': int(min_date.timestamp() * 1000),
-        'backtest_end': max_date,
+        'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
         'backtest_end_ts': int(max_date.timestamp() * 1000),
         'backtest_days': backtest_days,
 
@@ -362,9 +362,9 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
         strat_stats.update({
             'max_drawdown': max_drawdown,
             'max_drawdown_abs': drawdown_abs,
-            'drawdown_start': drawdown_start,
+            'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
             'drawdown_start_ts': drawdown_start.timestamp() * 1000,
-            'drawdown_end': drawdown_end,
+            'drawdown_end': drawdown_end.strftime(DATETIME_PRINT_FORMAT),
             'drawdown_end_ts': drawdown_end.timestamp() * 1000,
 
             'max_drawdown_low': low_val,
@@ -497,8 +497,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
         best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio'])
         worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])
         metrics = [
-            ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)),
-            ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)),
+            ('Backtesting from', strat_results['backtest_start']),
+            ('Backtesting to', strat_results['backtest_end']),
             ('Max open trades', strat_results['max_open_trades']),
             ('', ''),  # Empty line to improve readability
             ('Total trades', strat_results['total_trades']),
@@ -546,8 +546,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
                                                strat_results['stake_currency'])),
             ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
                                               strat_results['stake_currency'])),
-            ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
-            ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),
+            ('Drawdown Start', strat_results['drawdown_start']),
+            ('Drawdown End', strat_results['drawdown_end']),
             ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
         ]
 
diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py
index fb4624ca3..48ba416c0 100644
--- a/tests/optimize/test_optimize_reports.py
+++ b/tests/optimize/test_optimize_reports.py
@@ -7,7 +7,7 @@ import pytest
 from arrow import Arrow
 
 from freqtrade.configuration import TimeRange
-from freqtrade.constants import LAST_BT_RESULT_FN
+from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
 from freqtrade.data import history
 from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data
 from freqtrade.edge import PairInfo
@@ -97,8 +97,8 @@ def test_generate_backtest_stats(default_conf, testdatadir):
     assert 'DefStrat' in stats['strategy']
     assert 'strategy_comparison' in stats
     strat_stats = stats['strategy']['DefStrat']
-    assert strat_stats['backtest_start'] == min_date.datetime
-    assert strat_stats['backtest_end'] == max_date.datetime
+    assert strat_stats['backtest_start'] == min_date.strftime(DATETIME_PRINT_FORMAT)
+    assert strat_stats['backtest_end'] == max_date.strftime(DATETIME_PRINT_FORMAT)
     assert strat_stats['total_trades'] == len(results['DefStrat']['results'])
     # Above sample had no loosing trade
     assert strat_stats['max_drawdown'] == 0.0
@@ -141,8 +141,8 @@ def test_generate_backtest_stats(default_conf, testdatadir):
     strat_stats = stats['strategy']['DefStrat']
 
     assert strat_stats['max_drawdown'] == 0.013803
-    assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc)
-    assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc)
+    assert strat_stats['drawdown_start'] == '2017-11-14 22:10:00'
+    assert strat_stats['drawdown_end'] == '2017-11-14 22:43:00'
     assert strat_stats['drawdown_end_ts'] == 1510699380000
     assert strat_stats['drawdown_start_ts'] == 1510697400000
     assert strat_stats['pairlist'] == ['UNITTEST/BTC']

From 06bf1aa274be48f5c1d847349c34aa62fa1cdc67 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 05:58:25 +0200
Subject: [PATCH 0427/1386] Store epochs as json per line

---
 freqtrade/optimize/hyperopt.py  | 50 ++++++++++++++--------------
 tests/optimize/test_hyperopt.py | 59 ++++++++++++++++++---------------
 2 files changed, 57 insertions(+), 52 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 5ccf02d01..c14b62b9d 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -5,15 +5,16 @@ This module contains the hyperopt logic
 """
 
 import logging
+import os
 import random
 import warnings
 from datetime import datetime, timezone
 from math import ceil
-from operator import itemgetter
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
 import progressbar
+import rapidjson
 from colorama import Fore, Style
 from colorama import init as colorama_init
 from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
@@ -86,7 +87,7 @@ class Hyperopt:
         time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
         strategy = str(self.config['strategy'])
         self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' /
-                                   f'strategy_{strategy}_hyperopt_results_{time_now}.pickle')
+                                   f'strategy_{strategy}_hyperopt_results_{time_now}.fthypt')
         self.data_pickle_file = (self.config['user_data_dir'] /
                                  'hyperopt_results' / 'hyperopt_tickerdata.pkl')
         self.total_epochs = config.get('epochs', 0)
@@ -96,9 +97,7 @@ class Hyperopt:
         self.clean_hyperopt()
 
         self.num_epochs_saved = 0
-
-        # Previous evaluations
-        self.epochs: List = []
+        self.current_best_epoch: Optional[Dict[str, Any]] = None
 
         # Populate functions here (hasattr is slow so should not be run during "regular" operations)
         if hasattr(self.custom_hyperopt, 'populate_indicators'):
@@ -156,21 +155,24 @@ class Hyperopt:
         # and the values are taken from the list of parameters.
         return {d.name: v for d, v in zip(dimensions, raw_params)}
 
-    def _save_results(self) -> None:
+    def _save_result(self, epoch: Dict) -> None:
         """
         Save hyperopt results to file
+        Store one line per epoch.
+        While not a valid json object - this allows appending easily.
+        :param epoch: result dictionary for this epoch.
         """
-        num_epochs = len(self.epochs)
-        if num_epochs > self.num_epochs_saved:
-            logger.debug(f"Saving {num_epochs} {plural(num_epochs, 'epoch')}.")
-            dump(self.epochs, self.results_file)
-            self.num_epochs_saved = num_epochs
-            logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
-                         f"saved to '{self.results_file}'.")
-            # Store hyperopt filename
-            latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
-            file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
-                           log=False)
+        with self.results_file.open('a') as f:
+            rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE)
+            f.write(os.linesep)
+
+        self.num_epochs_saved += 1
+        logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
+                     f"saved to '{self.results_file}'.")
+        # Store hyperopt filename
+        latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
+        file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
+                       log=False)
 
     def _get_params_details(self, params: Dict) -> Dict:
         """
@@ -442,25 +444,21 @@ class Hyperopt:
 
                             if is_best:
                                 self.current_best_loss = val['loss']
-                            self.epochs.append(val)
+                                self.current_best_epoch = val
 
-                            # Save results after each best epoch and every 100 epochs
-                            if is_best or current % 100 == 0:
-                                self._save_results()
+                            self._save_result(val)
 
                             pbar.update(current)
 
         except KeyboardInterrupt:
             print('User interrupted..')
 
-        self._save_results()
         logger.info(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
                     f"saved to '{self.results_file}'.")
 
-        if self.epochs:
-            sorted_epochs = sorted(self.epochs, key=itemgetter('loss'))
-            best_epoch = sorted_epochs[0]
-            HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json)
+        if self.current_best_epoch:
+            HyperoptTools.print_epoch_details(self.current_best_epoch, self.total_epochs,
+                                              self.print_json)
         else:
             # This is printed when Ctrl+C is pressed quickly, before first epochs have
             # a chance to be evaluated.
diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
index 16b647df9..f9a9dba85 100644
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -386,7 +386,8 @@ def test_roi_table_generation(hyperopt) -> None:
 
 
 def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
 
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
@@ -425,9 +426,9 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
 
     out, err = capsys.readouterr()
     assert 'Best result:\n\n*    1/1: foo result Objective: 1.00000\n' in out
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    # Should be called for historical candle data
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
     assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
     assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
     assert hasattr(hyperopt, "max_open_trades")
@@ -714,7 +715,8 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog):
 
 
 def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
 
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
@@ -765,13 +767,14 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
         ':{},"stoploss":null,"trailing_stop":null}'
     )
     assert result_str in out  # noqa: E501
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    # Should be called for historical candle data
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
 
 
 def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -813,13 +816,14 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
 
     out, err = capsys.readouterr()
     assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null}' in out  # noqa: E501
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    # Should be called for historical candle data
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
 
 
 def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -860,13 +864,14 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
 
     out, err = capsys.readouterr()
     assert '{"minimal_roi":{},"stoploss":null}' in out
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
 
 
 def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -908,9 +913,9 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
 
     out, err = capsys.readouterr()
     assert 'Best result:\n\n*    1/1: foo result Objective: 1.00000\n' in out
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
+
     assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
     assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
     assert hasattr(hyperopt, "max_open_trades")
@@ -946,7 +951,8 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
 
 
 def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -989,8 +995,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
     out, err = capsys.readouterr()
     assert 'Best result:\n\n*    1/1: foo result Objective: 1.00000\n' in out
     assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
     assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
     assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
     assert hasattr(hyperopt, "max_open_trades")
@@ -999,7 +1005,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
 
 
 def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -1042,8 +1049,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
     out, err = capsys.readouterr()
     assert 'Best result:\n\n*    1/1: foo result Objective: 1.00000\n' in out
     assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
     assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
     assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
     assert hasattr(hyperopt, "max_open_trades")

From 3cbe40875de2e984bd01e42ae8905fe5e7b5a7e5 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 06:06:30 +0200
Subject: [PATCH 0428/1386] read hyperopt results from pickle or json

---
 freqtrade/optimize/hyperopt_tools.py | 25 ++++++++++++++++++++-----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py
index 8d6098257..665c393b3 100644
--- a/freqtrade/optimize/hyperopt_tools.py
+++ b/freqtrade/optimize/hyperopt_tools.py
@@ -31,15 +31,27 @@ class HyperoptTools():
         else:
             return any(s in config['spaces'] for s in [space, 'all', 'default'])
 
+    @staticmethod
+    def _read_results_pickle(results_file: Path) -> List:
+        """
+        Read hyperopt results from pickle file
+        LEGACY method - new files are written as json and cannot be read with this method.
+        """
+        from joblib import load
+
+        logger.info(f"Reading pickled epochs from '{results_file}'")
+        data = load(results_file)
+        return data
+
     @staticmethod
     def _read_results(results_file: Path) -> List:
         """
         Read hyperopt results from file
         """
-        from joblib import load
-
-        logger.info("Reading epochs from '%s'", results_file)
-        data = load(results_file)
+        import rapidjson
+        logger.info(f"Reading epochs from '{results_file}'")
+        with results_file.open('r') as f:
+            data = [rapidjson.loads(line) for line in f]
         return data
 
     @staticmethod
@@ -49,7 +61,10 @@ class HyperoptTools():
         """
         epochs: List = []
         if results_file.is_file() and results_file.stat().st_size > 0:
-            epochs = HyperoptTools._read_results(results_file)
+            if results_file.suffix == '.pickle':
+                epochs = HyperoptTools._read_results_pickle(results_file)
+            else:
+                epochs = HyperoptTools._read_results(results_file)
             # Detection of some old format, without 'is_best' field saved
             if epochs[0].get('is_best') is None:
                 raise OperationalException(

From ad4c51b3c5a36a089699931a05c566407a874bed Mon Sep 17 00:00:00 2001
From: Rokas Kupstys 
Date: Wed, 12 May 2021 09:30:35 +0300
Subject: [PATCH 0429/1386] * Added "Dataframe access" section showcasing how
 to obtain dataframe and use it to get last-available and trade-open candles.
 * Fix custom_sell() example to use rsi from last-available instead of
 trade-open candle, add a pointer to "Dataframe access" section for more info.
 * Simplify "Custom stoploss using an indicator from dataframe example"
 greatly, add a pointer to "Dataframe access" section for more info.

---
 docs/strategy-advanced.md | 100 +++++++++++++++++++++-----------------
 1 file changed, 55 insertions(+), 45 deletions(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index ada7a99be..0fd2b3c41 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -40,6 +40,41 @@ class AwesomeStrategy(IStrategy):
 !!! Note
     If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
 
+
+## Dataframe access
+
+You may access dataframe in various strategy functions by querying it from dataprovider.
+
+``` python
+from freqtrade.exchange import timeframe_to_prev_date
+
+class AwesomeStrategy(IStrategy):
+    def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
+                           rate: float, time_in_force: str, sell_reason: str,
+                           current_time: 'datetime', **kwargs) -> bool:
+        # Obtain pair dataframe.
+        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+
+        # Obtain last available candle. Do not use current_time to look up latest candle, because 
+        # current_time points to curret incomplete candle whose data is not available.
+        last_candle = dataframe.iloc[-1].squeeze()
+        # <...>
+
+        # In dry/live runs trade open date will not match candle open date therefore it must be 
+        # rounded.
+        trade_date = timeframe_to_prev_date(trade.open_date_utc)
+        # Look up trade candle.
+        trade_candle = dataframe.loc[dataframe['date'] == trade_date]
+        # trade_candle may be None for trades that just opened as it is stil lincomplete.
+        if trade_candle is not None:
+            # <...>
+```
+
+!!! Warning "Using .iloc[-1]"
+    You can use `.iloc[-1]` here because `get_analyzed_dataframe()` only returns candles that backtesting is allowed to see.
+    This will not work in `populate_*` methods, so make sure to not use `.iloc[]` in that area.
+    Also, this will only work starting with version 2021.5.
+
 ***
 
 ## Custom sell signal
@@ -62,19 +97,16 @@ class AwesomeStrategy(IStrategy):
     def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                     current_profit: float, **kwargs):
         dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
-        
-        # Get the row at trade open
-        trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
-        trade_open_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
+        last_candle = dataframe.iloc[-1].squeeze()
 
         # Above 20% profit, sell when rsi < 80
         if current_profit > 0.2:
-            if trade_open_row['rsi'] < 80:
+            if last_candle['rsi'] < 80:
                 return 'rsi_below_80'
 
         # Between 2% and 10%, sell if EMA-long above EMA-short
         if 0.02 < current_profit < 0.1:
-            if trade_open_row['emalong'] > trade_open_row['emashort']:
+            if last_candle['emalong'] > last_candle['emashort']:
                 return 'ema_long_below_80'
 
         # Sell any positions at a loss if they are held for more than one day.
@@ -82,7 +114,7 @@ class AwesomeStrategy(IStrategy):
             return 'unclog'
 ```
 
-See [Custom stoploss using an indicator from dataframe example](#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter.
+See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
 
 ## Custom stoploss
 
@@ -265,57 +297,35 @@ class AwesomeStrategy(IStrategy):
 
 #### Custom stoploss using an indicator from dataframe example
 
-Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR"
-
-!!! Note
-    `dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and
-    `trade.open_date_utc` will not match the candle date precisely and using them directly will throw
-    an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to the candle's open date
-    before using it to access `dataframe`.
+Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
 
 ``` python
-from freqtrade.exchange import timeframe_to_prev_date
-from freqtrade.persistence import Trade
-from freqtrade.state import RunMode
-
 class AwesomeStrategy(IStrategy):
 
-    # ... populate_* methods
+    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
+        # <...>
+        dataframe['sar'] = ta.SAR(dataframe)
 
     use_custom_stoploss = True
 
     def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
                         current_rate: float, current_profit: float, **kwargs) -> float:
-        # Default return value
-        result = 1
-        if trade:
-            # Using current_time directly would only work in backtesting. Live/dry runs need time to
-            # be rounded to previous candle to be used as dataframe index. Rounding must also be 
-            # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing.
-            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
-            current_candle = dataframe.iloc[-1].squeeze()
-            if 'atr' in current_candle:
-                # new stoploss relative to current_rate
-                new_stoploss = (current_rate - current_candle['atr']) / current_rate
 
-                # Round trade date to it's candle time.
-                trade_date = timeframe_to_prev_date(trade.open_date_utc)
-                trade_candle = dataframe.loc[dataframe['date'] == trade_date]
-                # Just opened trades do not have their candle complete yet therefore trade_candle may be None
-                if trade_candle is not None:
-                    trade_candle = trade_candle.squeeze()
-                    trade_stoploss = (current_rate - trade_candle['atr']) / current_rate
-                    new_stoploss = max(new_stoploss, trade_stoploss)
-                # turn into relative negative offset required by `custom_stoploss` return implementation
-                result = new_stoploss - 1
+        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+        last_candle = dataframe.iloc[-1].squeeze()
 
-        return result
+        # Use parabolic sar as absolute stoploss price
+        stoploss_price = last_candle['sar']
+
+        # Convert absolute price to percentage relative to current_rate
+        if stoploss_price < current_rate:
+            return (stoploss_price / current_rate) - 1
+
+        # return maximum stoploss value, keeping current stoploss price unchanged
+        return 1
 ```
 
-!!! Warning "Using .iloc[-1]"
-    You can use `.iloc[-1]` here because `get_analyzed_dataframe()` only returns candles that backtesting is allowed to see.
-    This will not work in `populate_*` methods, so make sure to not use `.iloc[]` in that area.
-    Also, this will only work starting with version 2021.5.
+See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
 
 ---
 

From 9bb6ba086bdeffe3b676cb0e0bf0e720b514fe8e Mon Sep 17 00:00:00 2001
From: Rokas Kupstys <19151258+rokups@users.noreply.github.com>
Date: Wed, 12 May 2021 17:15:38 +0300
Subject: [PATCH 0430/1386] Update docs/strategy-advanced.md

Co-authored-by: Matthias 
---
 docs/strategy-advanced.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index 0fd2b3c41..d533d81cd 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -65,7 +65,7 @@ class AwesomeStrategy(IStrategy):
         trade_date = timeframe_to_prev_date(trade.open_date_utc)
         # Look up trade candle.
         trade_candle = dataframe.loc[dataframe['date'] == trade_date]
-        # trade_candle may be None for trades that just opened as it is stil lincomplete.
+        # trade_candle may be None for trades that just opened as it is still incomplete.
         if trade_candle is not None:
             # <...>
 ```

From 5f5597b93fb43b8ed46bf1d1d2a88e4f3acf0b6a Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 06:32:55 +0200
Subject: [PATCH 0431/1386] Better test hyperopt writing and reading

---
 tests/optimize/test_hyperopt.py               |  85 +++++++-----------
 tests/optimize/test_optimize_reports.py       |   2 +-
 .../hyperopt_results_SampleStrategy.pickle    | Bin 0 -> 7436 bytes
 tests/testdata/strategy_SampleStrategy.fthypt |   5 ++
 4 files changed, 38 insertions(+), 54 deletions(-)
 create mode 100644 tests/testdata/hyperopt_results_SampleStrategy.pickle
 create mode 100644 tests/testdata/strategy_SampleStrategy.fthypt

diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
index f9a9dba85..a0c475efb 100644
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -31,23 +31,7 @@ from .hyperopts.default_hyperopt import DefaultHyperOpt
 
 
 # Functions for recurrent object patching
-def create_results(mocker, hyperopt, testdatadir) -> List[Dict]:
-    """
-    When creating results, mock the hyperopt so that *by default*
-      - we don't create any pickle'd files in the filesystem
-      - we might have a pickle'd file so make sure that we return
-        false when looking for it
-    """
-    hyperopt.results_file = testdatadir / 'optimize/ut_results.pickle'
-
-    mocker.patch.object(Path, "is_file", MagicMock(return_value=False))
-    stat_mock = MagicMock()
-    stat_mock.st_size = 1
-    mocker.patch.object(Path, "stat", MagicMock(return_value=stat_mock))
-
-    mocker.patch.object(Path, "unlink", MagicMock(return_value=True))
-    mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
-    mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
+def create_results() -> List[Dict]:
 
     return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}]
 
@@ -321,54 +305,49 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
     assert caplog.record_tuples == []
 
 
-def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
-    epochs = create_results(mocker, hyperopt, testdatadir)
-    mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
-    mock_dump_json = mocker.patch('freqtrade.optimize.hyperopt.file_dump_json', return_value=None)
-    results_file = testdatadir / 'optimize' / 'ut_results.pickle'
+def test_save_results_saves_epochs(mocker, hyperopt, tmpdir, caplog) -> None:
+    # Test writing to temp dir and reading again
+    epochs = create_results()
+    hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
 
     caplog.set_level(logging.DEBUG)
 
-    hyperopt.epochs = epochs
-    hyperopt._save_results()
-    assert log_has(f"1 epoch saved to '{results_file}'.", caplog)
-    mock_dump.assert_called_once()
-    mock_dump_json.assert_called_once()
+    for epoch in epochs:
+        hyperopt._save_result(epoch)
+    assert log_has(f"1 epoch saved to '{hyperopt.results_file}'.", caplog)
 
-    hyperopt.epochs = epochs + epochs
-    hyperopt._save_results()
-    assert log_has(f"2 epochs saved to '{results_file}'.", caplog)
+    hyperopt._save_result(epochs[0])
+    assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
+
+    hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file)
+    assert len(hyperopt_epochs) == 2
 
 
-def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
-    epochs = create_results(mocker, hyperopt, testdatadir)
-    mock_load = mocker.patch('joblib.load', return_value=epochs)
-    results_file = testdatadir / 'optimize' / 'ut_results.pickle'
-    hyperopt_epochs = HyperoptTools._read_results(results_file)
-    assert log_has(f"Reading epochs from '{results_file}'", caplog)
-    assert hyperopt_epochs == epochs
-    mock_load.assert_called_once()
+def test_load_previous_results(testdatadir, caplog) -> None:
 
-
-def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None:
-    epochs = create_results(mocker, hyperopt, testdatadir)
-    mock_load = mocker.patch('joblib.load', return_value=epochs)
-    mocker.patch.object(Path, 'is_file', MagicMock(return_value=True))
-    statmock = MagicMock()
-    statmock.st_size = 5
-    # mocker.patch.object(Path, 'stat', MagicMock(return_value=statmock))
-
-    results_file = testdatadir / 'optimize' / 'ut_results.pickle'
+    results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
 
     hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
 
-    assert hyperopt_epochs == epochs
-    mock_load.assert_called_once()
+    assert len(hyperopt_epochs) == 5
+    assert log_has_re(r"Reading pickled epochs from .*", caplog)
 
-    del epochs[0]['is_best']
-    mock_load = mocker.patch('joblib.load', return_value=epochs)
+    caplog.clear()
 
-    with pytest.raises(OperationalException):
+    # Modern version
+    results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
+
+    hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
+
+    assert len(hyperopt_epochs) == 5
+    assert log_has_re(r"Reading epochs from .*", caplog)
+
+
+def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
+    mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
+                 return_value=[{'asdf': '222'}])
+    results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
+    with pytest.raises(OperationalException, match=r"The file .* incompatible.*"):
         HyperoptTools.load_previous_results(results_file)
 
 
diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py
index 48ba416c0..1afa5c59c 100644
--- a/tests/optimize/test_optimize_reports.py
+++ b/tests/optimize/test_optimize_reports.py
@@ -1,5 +1,5 @@
 import re
-from datetime import datetime, timedelta, timezone
+from datetime import timedelta
 from pathlib import Path
 
 import pandas as pd
diff --git a/tests/testdata/hyperopt_results_SampleStrategy.pickle b/tests/testdata/hyperopt_results_SampleStrategy.pickle
new file mode 100644
index 0000000000000000000000000000000000000000..2231de7bf15c53c0a36380d1a158dd1bd08ee9d9
GIT binary patch
literal 7436
zcmb`MdvH|M9mh8hLLe_F5DZ`OSVr42xO0=-dKWXm;V>Ph_u-fYI}2W2_2A4-Gcp;k}f`;=+t^e#i+cw
zcnsq(itpUOm}^+G6xYX0zinsCB@mJj;oHMPrz$IDw6{z{2|V5K5uTR$I5HuHy1NBQ
zRZ8Q$6qJNsL5lNXRVj}P;c#7Mx@gyHnXbnWA9Hxi7%%#y>=?gg;gcQcQpSh&#zy-!
z!wB@CQVR8CiVyEo>gmHq&g2k!O}D
z`CWz$g(|GJ)3)%3UmpDI$BXH9BE7~)uWil6w>p16vuN`!#zuO@Hfp+{*^1+dXpEc<
z91FId|Hb}g{hz-*K%SaZ%JrinB=}G`PEJcfd$JGSpb=;s8jmKTN$6HI1x-hG8CA)s
zT1GQuG!xY%l|q<0;yMe>L32?Zav(S2kPp?%s3C>?G789O0a}<;3h<1AXPb?V!*}=D
z*3$f6(ov!o+Bm83&hokwN3}+BW?aTOM62k6bQl}kY)K^(+QJTBK^BsL^h?kGb!0W0qK!DPvW}!A36Vrl
zh($Y)+Lot`3B`kdt+(tTmoU%CmArC}B8~;+vpYMH$vFULA+v+pZ9{%LGX3JoQ?%6K#
zdS@UPxNbv_qQ}qK4{GYE52=8dqpM_cKqK#gw!dT!^
zkM}En6#1xNtM(9`z{U>hn<#ZMEY3XJb$utb4o2lCVf6LhG?h8d=?OSJ!*bc>@c1ma
z47|n7CN8_(oTnk+(YefV4zCrL-CTp;;nKkT1-gMYx8N(vzh6u0h&!eETG3RBW3~3W@HLs
zQXgYL-#GZ$l@;y#^g{~uc)I!x5WF2?Y&xJRPP
zL8gLC1E~V32B`s=g(W4Q5LqZ7mh;sNo3G=TU)7J@WlNr{WF
z%HnP0{bHg`Sj8n+%+P=D3&?3&-7l`dJ``G$$u9(_=xouWIN!J^;%e%Ok7&r)o{fG{
zg1*;b$t?jqt0w?QIstq)lK>`KCjf6fhNmAA0B4y1j#P$U{=@C3=>lfg*)(1)h9MHb
z-}xQ4-Lqwr_7FA!+_ksG&Kzs<6a4W3$=C%SY4Tz)g=jJ>*U#2qr
zKUH^JJlS5T&BySRuP|;Qt1`TbaWGMSB5TeDU?pQ_C~vw1$({0JEoxBfdYpyyÐB
z<@kT%UPrqz#px-)hTMB$U?rEeLyi#ZrDMvn(v-Ez0+F84fi5
z;Q6O1n`nP&%3IlV%FiPpKcIatyA0p{>BhyryRXpmJ@g%ixCc(eUXXnt2SA&}_
zki#HHu*l+3tSRv&qCX{i46Aq?i;?mtpz#z)KgiEO&VsxOat`D?$m>{A;sv5_V3ox;
ziC)AizJ~5vg}T2^Eaxm5X3&S&4g3|8g4I9I^s^XXN--Rp2$
zG2Z71I6Sn-7f0vg1uVmY8DB^kFP7uwG?D!~=eC{y{+nM8&`dWx*(VvdlNH&gF%BlO
zv)><;O?KAIkll2_l0|lzy;BLG{hJLRUDUpp4fQH~d9nS|SLk*f
z2I|w{2vmd20GS0c8)PoXJP-$n6H7{T5p`pgMUJQktLVjIgt`wJ{U8C5Mvz4yO(07^
zmVzw9k`iynDvK>dzd^JWtGFDC8R{@!(;n6?!^JzX4+T-N9kxPe?<3bO!2$)==kw=WcR4?zMvYf-h%3GhZhQZ)7_2B>+{z8y=kiB0=?d%a`$lcj(|q>M_>gXhvnYfHiA-J
J1*!{<{{b0q;2i(}

literal 0
HcmV?d00001

diff --git a/tests/testdata/strategy_SampleStrategy.fthypt b/tests/testdata/strategy_SampleStrategy.fthypt
new file mode 100644
index 000000000..6dc2b9ab1
--- /dev/null
+++ b/tests/testdata/strategy_SampleStrategy.fthypt
@@ -0,0 +1,5 @@
+{"loss":100000,"params_dict":{"mfi-value":"20","fastd-value":"21","adx-value":"26","rsi-value":"23","mfi-enabled":true,"fastd-enabled":false,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal","sell-mfi-value":"97","sell-fastd-value":"85","sell-adx-value":"55","sell-rsi-value":"76","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"34","roi_t2":"28","roi_t3":"32","roi_p1":0.031,"roi_p2":0.033,"roi_p3":0.146,"stoploss":-0.05},"params_details":{"buy":{"mfi-value":"20","fastd-value":"21","adx-value":"26","rsi-value":"23","mfi-enabled":true,"fastd-enabled":false,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"97","sell-fastd-value":"85","sell-adx-value":"55","sell-rsi-value":"76","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.21, 32: 0.064, 60: 0.031, 94: 0}","stoploss":{"stoploss":-0.05}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[],"locks":[],"best_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"worst_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"results_per_pair":[{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TRX/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"sell_reason_summary":[],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":0,"total_volume":0.0,"avg_stake_amount":0,"profit_mean":0,"profit_median":0,"profit_total":0.0,"profit_total_abs":0,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793107,"trades_per_day":0.0,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0,"backtest_worst_day":0,"backtest_best_day_abs":0,"backtest_worst_day_abs":0,"winning_days":0,"draw_days":0,"losing_days":0,"wins":0,"losses":0,"draws":0,"holding_avg":"0:00:00","winner_holding_avg":"0:00:00","loser_holding_avg":"0:00:00","max_drawdown":0.0,"max_drawdown_abs":0.0,"max_drawdown_low":0.0,"max_drawdown_high":0.0,"drawdown_start":"1970-01-01 00:00:00+00:00","drawdown_start_ts":0,"drawdown_end":"1970-01-01 00:00:00+00:00","drawdown_end_ts":0,"csum_min":0,"csum_max":0},"results_explanation":"     0 trades. 0/0/0 Wins/Draws/Losses. Avg profit   0.00%. Median profit   0.00%. Total profit  0.00000000 BTC (   0.00\u03A3%). Avg duration 0:00:00 min.","total_profit":0.0,"current_epoch":1,"is_initial_point":true,"is_best":false}
+{"loss":100000,"params_dict":{"mfi-value":"14","fastd-value":"43","adx-value":"30","rsi-value":"24","mfi-enabled":true,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal","sell-mfi-value":"97","sell-fastd-value":"71","sell-adx-value":"82","sell-rsi-value":"99","sell-mfi-enabled":false,"sell-fastd-enabled":false,"sell-adx-enabled":false,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"84","roi_t2":"35","roi_t3":"19","roi_p1":0.024,"roi_p2":0.022,"roi_p3":0.061,"stoploss":-0.083},"params_details":{"buy":{"mfi-value":"14","fastd-value":"43","adx-value":"30","rsi-value":"24","mfi-enabled":true,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"97","sell-fastd-value":"71","sell-adx-value":"82","sell-rsi-value":"99","sell-mfi-enabled":false,"sell-fastd-enabled":false,"sell-adx-enabled":false,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.107, 19: 0.046, 54: 0.024, 138: 0}","stoploss":{"stoploss":-0.083}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[],"locks":[],"best_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"worst_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"results_per_pair":[{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TRX/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"sell_reason_summary":[],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":0,"total_volume":0.0,"avg_stake_amount":0,"profit_mean":0,"profit_median":0,"profit_total":0.0,"profit_total_abs":0,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.0,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0,"backtest_worst_day":0,"backtest_best_day_abs":0,"backtest_worst_day_abs":0,"winning_days":0,"draw_days":0,"losing_days":0,"wins":0,"losses":0,"draws":0,"holding_avg":"0:00:00","winner_holding_avg":"0:00:00","loser_holding_avg":"0:00:00","max_drawdown":0.0,"max_drawdown_abs":0.0,"max_drawdown_low":0.0,"max_drawdown_high":0.0,"drawdown_start":"1970-01-01 00:00:00+00:00","drawdown_start_ts":0,"drawdown_end":"1970-01-01 00:00:00+00:00","drawdown_end_ts":0,"csum_min":0,"csum_max":0},"results_explanation":"     0 trades. 0/0/0 Wins/Draws/Losses. Avg profit   0.00%. Median profit   0.00%. Total profit  0.00000000 BTC (   0.00\u03A3%). Avg duration 0:00:00 min.","total_profit":0.0,"current_epoch":2,"is_initial_point":true,"is_best":false}
+{"loss":2.183447401951895,"params_dict":{"mfi-value":"14","fastd-value":"15","adx-value":"40","rsi-value":"36","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":false,"trigger":"sar_reversal","sell-mfi-value":"92","sell-fastd-value":"84","sell-adx-value":"61","sell-rsi-value":"61","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"68","roi_t2":"41","roi_t3":"21","roi_p1":0.015,"roi_p2":0.064,"roi_p3":0.126,"stoploss":-0.024},"params_details":{"buy":{"mfi-value":"14","fastd-value":"15","adx-value":"40","rsi-value":"36","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":false,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"92","sell-fastd-value":"84","sell-adx-value":"61","sell-rsi-value":"61","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.20500000000000002, 21: 0.079, 62: 0.015, 130: 0}","stoploss":{"stoploss":-0.024}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.94115571,"open_date":"2018-01-11 11:40:00+00:00","close_date":"2018-01-11 19:40:00+00:00","open_rate":0.01700012,"close_rate":0.017119538805820372,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":480,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.01659211712,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.01659211712,"stop_loss_ratio":-0.024,"min_rate":0.01689809,"max_rate":0.0171462,"is_open":false,"open_timestamp":1515670800000.0,"close_timestamp":1515699600000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.57407318,"open_date":"2018-01-12 11:05:00+00:00","close_date":"2018-01-12 12:30:00+00:00","open_rate":0.08709691,"close_rate":0.08901977203712995,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":0.08500658416,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.08500658416,"stop_loss_ratio":-0.024,"min_rate":0.08702974000000001,"max_rate":0.08929248000000001,"is_open":false,"open_timestamp":1515755100000.0,"close_timestamp":1515760200000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.93166236,"open_date":"2018-01-12 03:30:00+00:00","close_date":"2018-01-12 13:05:00+00:00","open_rate":0.01705517,"close_rate":0.01717497550928249,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":575,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016645845920000003,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016645845920000003,"stop_loss_ratio":-0.024,"min_rate":0.0169841,"max_rate":0.01719135,"is_open":false,"open_timestamp":1515727800000.0,"close_timestamp":1515762300000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.96876855,"open_date":"2018-01-13 03:50:00+00:00","close_date":"2018-01-13 06:05:00+00:00","open_rate":0.016842,"close_rate":0.016960308078273957,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016437792,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016437792,"stop_loss_ratio":-0.024,"min_rate":0.016836999999999998,"max_rate":0.017,"is_open":false,"open_timestamp":1515815400000.0,"close_timestamp":1515823500000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.53163205,"open_date":"2018-01-13 13:25:00+00:00","close_date":"2018-01-13 15:35:00+00:00","open_rate":0.09405001,"close_rate":0.09471067238835926,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":130,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.09179280976,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.09179280976,"stop_loss_ratio":-0.024,"min_rate":0.09369894000000001,"max_rate":0.09479997,"is_open":false,"open_timestamp":1515849900000.0,"close_timestamp":1515857700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.23816853,"open_date":"2018-01-13 15:30:00+00:00","close_date":"2018-01-13 16:20:00+00:00","open_rate":0.0025989999999999997,"close_rate":0.0028232990466633217,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":50,"profit_ratio":0.07872446,"profit_abs":0.00395,"sell_reason":"roi","initial_stop_loss_abs":0.002536624,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.002536624,"stop_loss_ratio":-0.024,"min_rate":0.00259525,"max_rate":0.0028288700000000003,"is_open":false,"open_timestamp":1515857400000.0,"close_timestamp":1515860400000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":492.80504632,"open_date":"2018-01-14 21:35:00+00:00","close_date":"2018-01-14 23:15:00+00:00","open_rate":0.00010146000000000001,"close_rate":0.00010369995985950828,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":100,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":9.902496e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.902496e-05,"stop_loss_ratio":-0.024,"min_rate":0.0001012,"max_rate":0.00010414,"is_open":false,"open_timestamp":1515965700000.0,"close_timestamp":1515971700000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.92398174,"open_date":"2018-01-15 12:45:00+00:00","close_date":"2018-01-15 21:05:00+00:00","open_rate":0.01709997,"close_rate":0.01722009021073758,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":500,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016689570719999998,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016689570719999998,"stop_loss_ratio":-0.024,"min_rate":0.01694,"max_rate":0.01725,"is_open":false,"open_timestamp":1516020300000.0,"close_timestamp":1516050300000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1111.60515785,"open_date":"2018-01-15 19:50:00+00:00","close_date":"2018-01-15 23:45:00+00:00","open_rate":4.4980000000000006e-05,"close_rate":4.390048e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":235,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":4.390048e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":4.390048e-05,"stop_loss_ratio":-0.024,"min_rate":4.409e-05,"max_rate":4.502e-05,"is_open":false,"open_timestamp":1516045800000.0,"close_timestamp":1516059900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":519.80455349,"open_date":"2018-01-21 03:55:00+00:00","close_date":"2018-01-21 04:05:00+00:00","open_rate":9.619e-05,"close_rate":9.388144e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":10,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":9.388144e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.388144e-05,"stop_loss_ratio":-0.024,"min_rate":9.568e-05,"max_rate":9.673e-05,"is_open":false,"open_timestamp":1516506900000.0,"close_timestamp":1516507500000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.029754,"open_date":"2018-01-20 22:15:00+00:00","close_date":"2018-01-21 07:45:00+00:00","open_rate":0.01650299,"close_rate":0.016106918239999997,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":570,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.016106918239999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016106918239999997,"stop_loss_ratio":-0.024,"min_rate":0.0162468,"max_rate":0.01663179,"is_open":false,"open_timestamp":1516486500000.0,"close_timestamp":1516520700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.75461832,"open_date":"2018-01-21 13:00:00+00:00","close_date":"2018-01-21 16:25:00+00:00","open_rate":0.00266601,"close_rate":0.00260202576,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":205,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.00260202576,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.00260202576,"stop_loss_ratio":-0.024,"min_rate":0.0026290800000000002,"max_rate":0.00269384,"is_open":false,"open_timestamp":1516539600000.0,"close_timestamp":1516551900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":552.18111541,"open_date":"2018-01-22 02:10:00+00:00","close_date":"2018-01-22 04:20:00+00:00","open_rate":9.055e-05,"close_rate":9.118607626693427e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":130,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":8.83768e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":8.83768e-05,"stop_loss_ratio":-0.024,"min_rate":9.013e-05,"max_rate":9.197e-05,"is_open":false,"open_timestamp":1516587000000.0,"close_timestamp":1516594800000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.99733237,"open_date":"2018-01-22 03:20:00+00:00","close_date":"2018-01-22 13:50:00+00:00","open_rate":0.0166815,"close_rate":0.016281143999999997,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":630,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.016281143999999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016281143999999997,"stop_loss_ratio":-0.024,"min_rate":0.01641443,"max_rate":0.016800000000000002,"is_open":false,"open_timestamp":1516591200000.0,"close_timestamp":1516629000000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":503.52467271,"open_date":"2018-01-23 08:55:00+00:00","close_date":"2018-01-23 09:40:00+00:00","open_rate":9.93e-05,"close_rate":9.69168e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":45,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":9.69168e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.69168e-05,"stop_loss_ratio":-0.024,"min_rate":9.754e-05,"max_rate":0.00010025,"is_open":false,"open_timestamp":1516697700000.0,"close_timestamp":1516700400000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.55148073,"open_date":"2018-01-24 02:10:00+00:00","close_date":"2018-01-24 04:40:00+00:00","open_rate":0.090665,"close_rate":0.09130188409433015,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":150,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.08848903999999999,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.08848903999999999,"stop_loss_ratio":-0.024,"min_rate":0.090665,"max_rate":0.09146000000000001,"is_open":false,"open_timestamp":1516759800000.0,"close_timestamp":1516768800000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.10584639,"open_date":"2018-01-24 19:20:00+00:00","close_date":"2018-01-24 21:35:00+00:00","open_rate":0.002617,"close_rate":0.0026353833416959357,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.002554192,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.002554192,"stop_loss_ratio":-0.024,"min_rate":0.002617,"max_rate":0.00264999,"is_open":false,"open_timestamp":1516821600000.0,"close_timestamp":1516829700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.34602691,"open_date":"2018-01-25 14:35:00+00:00","close_date":"2018-01-25 16:35:00+00:00","open_rate":0.00258451,"close_rate":0.002641568926241846,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":0.00252248176,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.00252248176,"stop_loss_ratio":-0.024,"min_rate":0.00258451,"max_rate":0.00264579,"is_open":false,"open_timestamp":1516890900000.0,"close_timestamp":1516898100000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.11910295,"open_date":"2018-01-24 23:05:00+00:00","close_date":"2018-01-25 16:55:00+00:00","open_rate":0.01603025,"close_rate":0.016142855870546913,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":1070,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.015645523999999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.015645523999999997,"stop_loss_ratio":-0.024,"min_rate":0.015798760000000002,"max_rate":0.01617,"is_open":false,"open_timestamp":1516835100000.0,"close_timestamp":1516899300000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":553.70985604,"open_date":"2018-01-26 19:30:00+00:00","close_date":"2018-01-26 23:30:00+00:00","open_rate":9.03e-05,"close_rate":9.093432012042147e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":240,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":8.813279999999999e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":8.813279999999999e-05,"stop_loss_ratio":-0.024,"min_rate":8.961e-05,"max_rate":9.1e-05,"is_open":false,"open_timestamp":1516995000000.0,"close_timestamp":1517009400000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.22929005,"open_date":"2018-01-26 21:15:00+00:00","close_date":"2018-01-28 03:50:00+00:00","open_rate":0.0026002,"close_rate":0.0026184653286502758,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":1835,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0025377952,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.0025377952,"stop_loss_ratio":-0.024,"min_rate":0.00254702,"max_rate":0.00262797,"is_open":false,"open_timestamp":1517001300000.0,"close_timestamp":1517111400000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.15677093,"open_date":"2018-01-27 22:05:00+00:00","close_date":"2018-01-28 10:45:00+00:00","open_rate":0.01583897,"close_rate":0.015950232207727046,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":760,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.01545883472,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.01545883472,"stop_loss_ratio":-0.024,"min_rate":0.015700000000000002,"max_rate":0.01596521,"is_open":false,"open_timestamp":1517090700000.0,"close_timestamp":1517136300000.0}],"locks":[],"best_pair":{"key":"ETC/BTC","trades":5,"profit_mean":0.012572794000000002,"profit_mean_pct":1.2572794000000003,"profit_sum":0.06286397,"profit_sum_pct":6.29,"profit_total_abs":0.0031542000000000002,"profit_total":3.1542000000000002e-06,"profit_total_pct":0.0,"duration_avg":"7:49:00","wins":2,"draws":2,"losses":1},"worst_pair":{"key":"LTC/BTC","trades":8,"profit_mean":-0.0077020425,"profit_mean_pct":-0.77020425,"profit_sum":-0.06161634,"profit_sum_pct":-6.16,"profit_total_abs":-0.0030916,"profit_total":-3.0915999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"9:50:00","wins":0,"draws":6,"losses":2},"results_per_pair":[{"key":"ETC/BTC","trades":5,"profit_mean":0.012572794000000002,"profit_mean_pct":1.2572794000000003,"profit_sum":0.06286397,"profit_sum_pct":6.29,"profit_total_abs":0.0031542000000000002,"profit_total":3.1542000000000002e-06,"profit_total_pct":0.0,"duration_avg":"7:49:00","wins":2,"draws":2,"losses":1},{"key":"ETH/BTC","trades":3,"profit_mean":0.00498256,"profit_mean_pct":0.498256,"profit_sum":0.01494768,"profit_sum_pct":1.49,"profit_total_abs":0.00075,"profit_total":7.5e-07,"profit_total_pct":0.0,"duration_avg":"2:02:00","wins":1,"draws":2,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":1,"profit_mean":-0.03080817,"profit_mean_pct":-3.080817,"profit_sum":-0.03080817,"profit_sum_pct":-3.08,"profit_total_abs":-0.0015458,"profit_total":-1.5457999999999999e-06,"profit_total_pct":-0.0,"duration_avg":"3:55:00","wins":0,"draws":0,"losses":1},{"key":"TRX/BTC","trades":5,"profit_mean":-0.009333732,"profit_mean_pct":-0.9333732000000001,"profit_sum":-0.04666866,"profit_sum_pct":-4.67,"profit_total_abs":-0.0023416,"profit_total":-2.3416e-06,"profit_total_pct":-0.0,"duration_avg":"1:45:00","wins":1,"draws":2,"losses":2},{"key":"LTC/BTC","trades":8,"profit_mean":-0.0077020425,"profit_mean_pct":-0.77020425,"profit_sum":-0.06161634,"profit_sum_pct":-6.16,"profit_total_abs":-0.0030916,"profit_total":-3.0915999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"9:50:00","wins":0,"draws":6,"losses":2},{"key":"TOTAL","trades":22,"profit_mean":-0.0027855236363636365,"profit_mean_pct":-0.27855236363636365,"profit_sum":-0.06128152,"profit_sum_pct":-6.13,"profit_total_abs":-0.0030748,"profit_total":-3.0747999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"6:12:00","wins":4,"draws":12,"losses":6}],"sell_reason_summary":[{"sell_reason":"roi","trades":16,"wins":4,"draws":12,"losses":0,"profit_mean":0.00772296875,"profit_mean_pct":0.77,"profit_sum":0.1235675,"profit_sum_pct":12.36,"profit_total_abs":0.006200000000000001,"profit_total":0.041189166666666666,"profit_total_pct":4.12},{"sell_reason":"stop_loss","trades":6,"wins":0,"draws":0,"losses":6,"profit_mean":-0.03080817,"profit_mean_pct":-3.08,"profit_sum":-0.18484902,"profit_sum_pct":-18.48,"profit_total_abs":-0.0092748,"profit_total":-0.06161634,"profit_total_pct":-6.16}],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":22,"total_volume":1.1000000000000003,"avg_stake_amount":0.05000000000000002,"profit_mean":-0.0027855236363636365,"profit_median":0.0,"profit_total":-3.0747999999999998e-06,"profit_total_abs":-0.0030748,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":1.16,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":999.9969252,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.07872446,"backtest_worst_day":-0.09242451,"backtest_best_day_abs":0.00395,"backtest_worst_day_abs":-0.0046374,"winning_days":4,"draw_days":10,"losing_days":4,"wins":4,"losses":6,"draws":12,"holding_avg":"6:12:00","winner_holding_avg":"1:29:00","loser_holding_avg":"4:42:00","max_drawdown":0.18484901999999998,"max_drawdown_abs":0.0092748,"drawdown_start":"2018-01-14 23:15:00","drawdown_start_ts":1515971700000.0,"drawdown_end":"2018-01-23 09:40:00","drawdown_end_ts":1516700400000.0,"max_drawdown_low":-0.0038247999999999997,"max_drawdown_high":0.00545,"csum_min":999.9961752,"csum_max":1000.00545},"results_explanation":"    22 trades. 4/12/6 Wins/Draws/Losses. Avg profit  -0.28%. Median profit   0.00%. Total profit -0.00307480 BTC (  -0.00\u03A3%). Avg duration 6:12:00 min.","total_profit":-3.0747999999999998e-06,"current_epoch":3,"is_initial_point":true,"is_best":true}
+{"loss":-4.9544427978437175,"params_dict":{"mfi-value":"23","fastd-value":"40","adx-value":"50","rsi-value":"27","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":true,"rsi-enabled":true,"trigger":"bb_lower","sell-mfi-value":"87","sell-fastd-value":"60","sell-adx-value":"81","sell-rsi-value":"69","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":false,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal","roi_t1":"105","roi_t2":"43","roi_t3":"12","roi_p1":0.03,"roi_p2":0.036,"roi_p3":0.103,"stoploss":-0.081},"params_details":{"buy":{"mfi-value":"23","fastd-value":"40","adx-value":"50","rsi-value":"27","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":true,"rsi-enabled":true,"trigger":"bb_lower"},"sell":{"sell-mfi-value":"87","sell-fastd-value":"60","sell-adx-value":"81","sell-rsi-value":"69","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":false,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal"},"roi":"{0: 0.16899999999999998, 12: 0.066, 55: 0.03, 160: 0}","stoploss":{"stoploss":-0.081}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"XLM/BTC","stake_amount":0.05,"amount":1086.95652174,"open_date":"2018-01-13 13:30:00+00:00","close_date":"2018-01-13 16:30:00+00:00","open_rate":4.6e-05,"close_rate":4.632313095835424e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":180,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.2274e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.2274e-05,"stop_loss_ratio":-0.081,"min_rate":4.4980000000000006e-05,"max_rate":4.673e-05,"is_open":false,"open_timestamp":1515850200000.0,"close_timestamp":1515861000000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":851.35365231,"open_date":"2018-01-15 14:50:00+00:00","close_date":"2018-01-15 16:15:00+00:00","open_rate":5.873000000000001e-05,"close_rate":6.0910642247867544e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":5.397287000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.397287000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.873000000000001e-05,"max_rate":6.120000000000001e-05,"is_open":false,"open_timestamp":1516027800000.0,"close_timestamp":1516032900000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":896.86098655,"open_date":"2018-01-16 00:35:00+00:00","close_date":"2018-01-16 03:15:00+00:00","open_rate":5.575000000000001e-05,"close_rate":5.6960000000000004e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.01457705,"profit_abs":0.0007314,"sell_reason":"roi","initial_stop_loss_abs":5.123425000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.123425000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.575000000000001e-05,"max_rate":5.730000000000001e-05,"is_open":false,"open_timestamp":1516062900000.0,"close_timestamp":1516072500000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":747.160789,"open_date":"2018-01-16 22:30:00+00:00","close_date":"2018-01-16 22:45:00+00:00","open_rate":6.692e-05,"close_rate":7.182231811339689e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":15,"profit_ratio":0.06576981,"profit_abs":0.0033,"sell_reason":"roi","initial_stop_loss_abs":6.149948000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":6.149948000000001e-05,"stop_loss_ratio":-0.081,"min_rate":6.692e-05,"max_rate":7.566e-05,"is_open":false,"open_timestamp":1516141800000.0,"close_timestamp":1516142700000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":720.5649229,"open_date":"2018-01-17 15:15:00+00:00","close_date":"2018-01-17 16:40:00+00:00","open_rate":6.939000000000001e-05,"close_rate":7.19664475664827e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":6.376941000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":6.376941000000001e-05,"stop_loss_ratio":-0.081,"min_rate":6.758e-05,"max_rate":7.244e-05,"is_open":false,"open_timestamp":1516202100000.0,"close_timestamp":1516207200000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1144.42664225,"open_date":"2018-01-18 22:20:00+00:00","close_date":"2018-01-19 00:35:00+00:00","open_rate":4.3690000000000004e-05,"close_rate":4.531220772704466e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":4.015111e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.015111e-05,"stop_loss_ratio":-0.081,"min_rate":4.3690000000000004e-05,"max_rate":4.779e-05,"is_open":false,"open_timestamp":1516314000000.0,"close_timestamp":1516322100000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":876.57784011,"open_date":"2018-01-18 22:25:00+00:00","close_date":"2018-01-19 01:05:00+00:00","open_rate":5.704e-05,"close_rate":5.792e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.00834457,"profit_abs":0.00041869,"sell_reason":"roi","initial_stop_loss_abs":5.2419760000000006e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.2419760000000006e-05,"stop_loss_ratio":-0.081,"min_rate":5.704e-05,"max_rate":5.8670000000000006e-05,"is_open":false,"open_timestamp":1516314300000.0,"close_timestamp":1516323900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":525.59655209,"open_date":"2018-01-20 05:05:00+00:00","close_date":"2018-01-20 06:25:00+00:00","open_rate":9.513e-05,"close_rate":9.86621726041144e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":80,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":8.742447000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":8.742447000000001e-05,"stop_loss_ratio":-0.081,"min_rate":9.513e-05,"max_rate":9.95e-05,"is_open":false,"open_timestamp":1516424700000.0,"close_timestamp":1516429500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":920.64076597,"open_date":"2018-01-26 07:40:00+00:00","close_date":"2018-01-26 10:20:00+00:00","open_rate":5.431000000000001e-05,"close_rate":5.474000000000001e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.0008867,"profit_abs":4.449e-05,"sell_reason":"roi","initial_stop_loss_abs":4.991089000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.991089000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.3670000000000006e-05,"max_rate":5.5e-05,"is_open":false,"open_timestamp":1516952400000.0,"close_timestamp":1516962000000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":944.28706327,"open_date":"2018-01-28 04:35:00+00:00","close_date":"2018-01-30 04:45:00+00:00","open_rate":5.2950000000000006e-05,"close_rate":4.995000000000001e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":2890,"profit_ratio":-0.06323759,"profit_abs":-0.00317295,"sell_reason":"force_sell","initial_stop_loss_abs":4.866105000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.866105000000001e-05,"stop_loss_ratio":-0.081,"min_rate":4.980000000000001e-05,"max_rate":5.3280000000000005e-05,"is_open":true,"open_timestamp":1517114100000.0,"close_timestamp":1517287500000.0}],"locks":[],"best_pair":{"key":"TRX/BTC","trades":3,"profit_mean":0.04185351666666667,"profit_mean_pct":4.185351666666667,"profit_sum":0.12556055,"profit_sum_pct":12.56,"profit_total_abs":0.0063,"profit_total":6.3e-06,"profit_total_pct":0.0,"duration_avg":"1:00:00","wins":3,"draws":0,"losses":0},"worst_pair":{"key":"XLM/BTC","trades":3,"profit_mean":-0.01111407333333333,"profit_mean_pct":-1.111407333333333,"profit_sum":-0.03334221999999999,"profit_sum_pct":-3.33,"profit_total_abs":-0.0016729499999999999,"profit_total":-1.6729499999999998e-06,"profit_total_pct":-0.0,"duration_avg":"17:48:00","wins":1,"draws":1,"losses":1},"results_per_pair":[{"key":"TRX/BTC","trades":3,"profit_mean":0.04185351666666667,"profit_mean_pct":4.185351666666667,"profit_sum":0.12556055,"profit_sum_pct":12.56,"profit_total_abs":0.0063,"profit_total":6.3e-06,"profit_total_pct":0.0,"duration_avg":"1:00:00","wins":3,"draws":0,"losses":0},{"key":"ADA/BTC","trades":4,"profit_mean":0.0134259225,"profit_mean_pct":1.34259225,"profit_sum":0.05370369,"profit_sum_pct":5.37,"profit_total_abs":0.00269458,"profit_total":2.69458e-06,"profit_total_pct":0.0,"duration_avg":"2:21:00","wins":4,"draws":0,"losses":0},{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":3,"profit_mean":-0.01111407333333333,"profit_mean_pct":-1.111407333333333,"profit_sum":-0.03334221999999999,"profit_sum_pct":-3.33,"profit_total_abs":-0.0016729499999999999,"profit_total":-1.6729499999999998e-06,"profit_total_pct":-0.0,"duration_avg":"17:48:00","wins":1,"draws":1,"losses":1},{"key":"TOTAL","trades":10,"profit_mean":0.014592201999999999,"profit_mean_pct":1.4592201999999999,"profit_sum":0.14592201999999999,"profit_sum_pct":14.59,"profit_total_abs":0.00732163,"profit_total":7.32163e-06,"profit_total_pct":0.0,"duration_avg":"6:35:00","wins":8,"draws":1,"losses":1}],"sell_reason_summary":[{"sell_reason":"roi","trades":9,"wins":8,"draws":1,"losses":0,"profit_mean":0.023239956666666665,"profit_mean_pct":2.32,"profit_sum":0.20915961,"profit_sum_pct":20.92,"profit_total_abs":0.01049458,"profit_total":0.06971987,"profit_total_pct":6.97},{"sell_reason":"force_sell","trades":1,"wins":0,"draws":0,"losses":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.32,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-0.021079196666666664,"profit_total_pct":-2.11}],"left_open_trades":[{"key":"XLM/BTC","trades":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.323759,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-3.17295e-06,"profit_total_pct":-0.0,"duration_avg":"2 days, 0:10:00","wins":0,"draws":0,"losses":1},{"key":"TOTAL","trades":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.323759,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-3.17295e-06,"profit_total_pct":-0.0,"duration_avg":"2 days, 0:10:00","wins":0,"draws":0,"losses":1}],"total_trades":10,"total_volume":0.5,"avg_stake_amount":0.05,"profit_mean":0.014592201999999999,"profit_median":0.02223621,"profit_total":7.32163e-06,"profit_total_abs":0.00732163,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.53,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000.00732163,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.08034685999999999,"backtest_worst_day":-0.06323759,"backtest_best_day_abs":0.0040314,"backtest_worst_day_abs":-0.00317295,"winning_days":6,"draw_days":11,"losing_days":1,"wins":8,"losses":1,"draws":1,"holding_avg":"6:35:00","winner_holding_avg":"1:50:00","loser_holding_avg":"2 days, 0:10:00","max_drawdown":0.06323759000000001,"max_drawdown_abs":0.00317295,"drawdown_start":"2018-01-26 10:20:00","drawdown_start_ts":1516962000000.0,"drawdown_end":"2018-01-30 04:45:00","drawdown_end_ts":1517287500000.0,"max_drawdown_low":0.007321629999999998,"max_drawdown_high":0.010494579999999998,"csum_min":1000.0,"csum_max":1000.01049458},"results_explanation":"    10 trades. 8/1/1 Wins/Draws/Losses. Avg profit   1.46%. Median profit   2.22%. Total profit  0.00732163 BTC (   0.00\u03A3%). Avg duration 6:35:00 min.","total_profit":7.32163e-06,"current_epoch":4,"is_initial_point":true,"is_best":true}
+{"loss":0.16709185414267655,"params_dict":{"mfi-value":"10","fastd-value":"45","adx-value":"28","rsi-value":"37","mfi-enabled":false,"fastd-enabled":false,"adx-enabled":true,"rsi-enabled":true,"trigger":"macd_cross_signal","sell-mfi-value":"85","sell-fastd-value":"56","sell-adx-value":"98","sell-rsi-value":"89","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal","roi_t1":"85","roi_t2":"11","roi_t3":"24","roi_p1":0.04,"roi_p2":0.043,"roi_p3":0.053,"stoploss":-0.057},"params_details":{"buy":{"mfi-value":"10","fastd-value":"45","adx-value":"28","rsi-value":"37","mfi-enabled":false,"fastd-enabled":false,"adx-enabled":true,"rsi-enabled":true,"trigger":"macd_cross_signal"},"sell":{"sell-mfi-value":"85","sell-fastd-value":"56","sell-adx-value":"98","sell-rsi-value":"89","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal"},"roi":"{0: 0.13599999999999998, 24: 0.08299999999999999, 35: 0.04, 120: 0}","stoploss":{"stoploss":-0.057}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.56173464,"open_date":"2018-01-10 19:15:00+00:00","close_date":"2018-01-10 21:15:00+00:00","open_rate":0.08901,"close_rate":0.09112999000000001,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.01667571,"profit_abs":0.0008367,"sell_reason":"roi","initial_stop_loss_abs":0.08393643,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.08393643,"stop_loss_ratio":-0.057,"min_rate":0.08894498,"max_rate":0.09116998,"is_open":false,"open_timestamp":1515611700000.0,"close_timestamp":1515618900000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":794.65988557,"open_date":"2018-01-13 11:30:00+00:00","close_date":"2018-01-13 15:10:00+00:00","open_rate":6.292e-05,"close_rate":5.9333559999999994e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":220,"profit_ratio":-0.06357798,"profit_abs":-0.00319003,"sell_reason":"stop_loss","initial_stop_loss_abs":5.9333559999999994e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.9333559999999994e-05,"stop_loss_ratio":-0.057,"min_rate":5.9900000000000006e-05,"max_rate":6.353e-05,"is_open":false,"open_timestamp":1515843000000.0,"close_timestamp":1515856200000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1086.95652174,"open_date":"2018-01-13 14:35:00+00:00","close_date":"2018-01-13 21:40:00+00:00","open_rate":4.6e-05,"close_rate":4.632313095835424e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":425,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.3378e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":4.3378e-05,"stop_loss_ratio":-0.057,"min_rate":4.4980000000000006e-05,"max_rate":4.6540000000000005e-05,"is_open":false,"open_timestamp":1515854100000.0,"close_timestamp":1515879600000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.53757603,"open_date":"2018-01-15 13:15:00+00:00","close_date":"2018-01-15 15:15:00+00:00","open_rate":0.0930101,"close_rate":0.09366345745107878,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0877085243,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0877085243,"stop_loss_ratio":-0.057,"min_rate":0.09188489999999999,"max_rate":0.09380000000000001,"is_open":false,"open_timestamp":1516022100000.0,"close_timestamp":1516029300000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":17.07469496,"open_date":"2018-01-15 14:35:00+00:00","close_date":"2018-01-15 16:35:00+00:00","open_rate":0.00292831,"close_rate":0.00297503,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.00886772,"profit_abs":0.00044494,"sell_reason":"roi","initial_stop_loss_abs":0.0027613963299999997,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0027613963299999997,"stop_loss_ratio":-0.057,"min_rate":0.00292831,"max_rate":0.00301259,"is_open":false,"open_timestamp":1516026900000.0,"close_timestamp":1516034100000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":702.44450688,"open_date":"2018-01-17 04:25:00+00:00","close_date":"2018-01-17 05:00:00+00:00","open_rate":7.118e-05,"close_rate":7.453721023582538e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":35,"profit_ratio":0.03986049,"profit_abs":0.002,"sell_reason":"roi","initial_stop_loss_abs":6.712274e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":6.712274e-05,"stop_loss_ratio":-0.057,"min_rate":7.118e-05,"max_rate":7.658000000000002e-05,"is_open":false,"open_timestamp":1516163100000.0,"close_timestamp":1516165200000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.86756854,"open_date":"2018-01-20 06:05:00+00:00","close_date":"2018-01-20 08:05:00+00:00","open_rate":0.00265005,"close_rate":0.00266995,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.00048133,"profit_abs":2.415e-05,"sell_reason":"roi","initial_stop_loss_abs":0.00249899715,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.00249899715,"stop_loss_ratio":-0.057,"min_rate":0.00265005,"max_rate":0.00271,"is_open":false,"open_timestamp":1516428300000.0,"close_timestamp":1516435500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":966.18357488,"open_date":"2018-01-22 03:25:00+00:00","close_date":"2018-01-22 07:05:00+00:00","open_rate":5.1750000000000004e-05,"close_rate":5.211352232814853e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":220,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.8800250000000004e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":4.8800250000000004e-05,"stop_loss_ratio":-0.057,"min_rate":5.1750000000000004e-05,"max_rate":5.2170000000000004e-05,"is_open":false,"open_timestamp":1516591500000.0,"close_timestamp":1516604700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.95303438,"open_date":"2018-01-23 13:10:00+00:00","close_date":"2018-01-23 16:00:00+00:00","open_rate":0.0026381,"close_rate":0.002656631560461616,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":170,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0024877283,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0024877283,"stop_loss_ratio":-0.057,"min_rate":0.0026100000000000003,"max_rate":0.00266,"is_open":false,"open_timestamp":1516713000000.0,"close_timestamp":1516723200000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":912.40875912,"open_date":"2018-01-26 06:30:00+00:00","close_date":"2018-01-26 10:45:00+00:00","open_rate":5.480000000000001e-05,"close_rate":5.518494731560462e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":255,"profit_ratio":-0.0,"profit_abs":-0.0,"sell_reason":"roi","initial_stop_loss_abs":5.1676400000000006e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.1676400000000006e-05,"stop_loss_ratio":-0.057,"min_rate":5.3670000000000006e-05,"max_rate":5.523e-05,"is_open":false,"open_timestamp":1516948200000.0,"close_timestamp":1516963500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":909.58704748,"open_date":"2018-01-27 02:10:00+00:00","close_date":"2018-01-27 05:40:00+00:00","open_rate":5.4970000000000004e-05,"close_rate":5.535614149523332e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":210,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":5.183671e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.183671e-05,"stop_loss_ratio":-0.057,"min_rate":5.472000000000001e-05,"max_rate":5.556e-05,"is_open":false,"open_timestamp":1517019000000.0,"close_timestamp":1517031600000.0}],"locks":[],"best_pair":{"key":"TRX/BTC","trades":1,"profit_mean":0.03986049,"profit_mean_pct":3.986049,"profit_sum":0.03986049,"profit_sum_pct":3.99,"profit_total_abs":0.002,"profit_total":2e-06,"profit_total_pct":0.0,"duration_avg":"0:35:00","wins":1,"draws":0,"losses":0},"worst_pair":{"key":"ADA/BTC","trades":4,"profit_mean":-0.015894495,"profit_mean_pct":-1.5894495000000002,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-3.19003e-06,"profit_total_pct":-0.0,"duration_avg":"3:46:00","wins":0,"draws":3,"losses":1},"results_per_pair":[{"key":"TRX/BTC","trades":1,"profit_mean":0.03986049,"profit_mean_pct":3.986049,"profit_sum":0.03986049,"profit_sum_pct":3.99,"profit_total_abs":0.002,"profit_total":2e-06,"profit_total_pct":0.0,"duration_avg":"0:35:00","wins":1,"draws":0,"losses":0},{"key":"ETH/BTC","trades":2,"profit_mean":0.008337855,"profit_mean_pct":0.8337855,"profit_sum":0.01667571,"profit_sum_pct":1.67,"profit_total_abs":0.0008367,"profit_total":8.367e-07,"profit_total_pct":0.0,"duration_avg":"2:00:00","wins":1,"draws":1,"losses":0},{"key":"ETC/BTC","trades":3,"profit_mean":0.0031163500000000004,"profit_mean_pct":0.31163500000000005,"profit_sum":0.009349050000000001,"profit_sum_pct":0.93,"profit_total_abs":0.00046909,"profit_total":4.6909000000000003e-07,"profit_total_pct":0.0,"duration_avg":"2:17:00","wins":2,"draws":1,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":1,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"7:05:00","wins":0,"draws":1,"losses":0},{"key":"ADA/BTC","trades":4,"profit_mean":-0.015894495,"profit_mean_pct":-1.5894495000000002,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-3.19003e-06,"profit_total_pct":-0.0,"duration_avg":"3:46:00","wins":0,"draws":3,"losses":1},{"key":"TOTAL","trades":11,"profit_mean":0.00020975181818181756,"profit_mean_pct":0.020975181818181757,"profit_sum":0.002307269999999993,"profit_sum_pct":0.23,"profit_total_abs":0.00011576000000000034,"profit_total":1.1576000000000034e-07,"profit_total_pct":0.0,"duration_avg":"3:03:00","wins":4,"draws":6,"losses":1}],"sell_reason_summary":[{"sell_reason":"roi","trades":10,"wins":4,"draws":6,"losses":0,"profit_mean":0.0065885250000000005,"profit_mean_pct":0.66,"profit_sum":0.06588525,"profit_sum_pct":6.59,"profit_total_abs":0.0033057900000000003,"profit_total":0.021961750000000002,"profit_total_pct":2.2},{"sell_reason":"stop_loss","trades":1,"wins":0,"draws":0,"losses":1,"profit_mean":-0.06357798,"profit_mean_pct":-6.36,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-0.021192660000000002,"profit_total_pct":-2.12}],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":11,"total_volume":0.55,"avg_stake_amount":0.05,"profit_mean":0.00020975181818181756,"profit_median":0.0,"profit_total":1.1576000000000034e-07,"profit_total_abs":0.00011576000000000034,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.58,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000.00011576,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.03986049,"backtest_worst_day":-0.06357798,"backtest_best_day_abs":0.002,"backtest_worst_day_abs":-0.00319003,"winning_days":4,"draw_days":13,"losing_days":1,"wins":4,"losses":1,"draws":6,"holding_avg":"3:03:00","winner_holding_avg":"1:39:00","loser_holding_avg":"3:40:00","max_drawdown":0.06357798,"max_drawdown_abs":0.00319003,"drawdown_start":"2018-01-10 21:15:00","drawdown_start_ts":1515618900000.0,"drawdown_end":"2018-01-13 15:10:00","drawdown_end_ts":1515856200000.0,"max_drawdown_low":-0.00235333,"max_drawdown_high":0.0008367,"csum_min":999.99764667,"csum_max":1000.0008367},"results_explanation":"    11 trades. 4/6/1 Wins/Draws/Losses. Avg profit   0.02%. Median profit   0.00%. Total profit  0.00011576 BTC (   0.00\u03A3%). Avg duration 3:03:00 min.","total_profit":1.1576000000000034e-07,"current_epoch":5,"is_initial_point":true,"is_best":false}

From 24a1d5a96f09cfeb4ad5105bc98ac4c976f0cb30 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 19:04:32 +0200
Subject: [PATCH 0432/1386] Change default hyperopt-name to be shorter

---
 freqtrade/optimize/hyperopt.py       | 2 +-
 freqtrade/optimize/hyperopt_tools.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index c14b62b9d..422879451 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -87,7 +87,7 @@ class Hyperopt:
         time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
         strategy = str(self.config['strategy'])
         self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' /
-                                   f'strategy_{strategy}_hyperopt_results_{time_now}.fthypt')
+                                   f'strategy_{strategy}_{time_now}.fthypt')
         self.data_pickle_file = (self.config['user_data_dir'] /
                                  'hyperopt_results' / 'hyperopt_tickerdata.pkl')
         self.total_epochs = config.get('epochs', 0)
diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py
index 665c393b3..49e70913f 100644
--- a/freqtrade/optimize/hyperopt_tools.py
+++ b/freqtrade/optimize/hyperopt_tools.py
@@ -182,7 +182,7 @@ class HyperoptTools():
 
     @staticmethod
     def is_best_loss(results, current_best_loss: float) -> bool:
-        return results['loss'] < current_best_loss
+        return bool(results['loss'] < current_best_loss)
 
     @staticmethod
     def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str:

From 5e66d37d57927e1275a751ca5a4c78938040b4a0 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 20:07:45 +0200
Subject: [PATCH 0433/1386] Slightly modify docker instructions for arm64

---
 docs/docker_quickstart.md | 26 ++++++++++++++++----------
 1 file changed, 16 insertions(+), 10 deletions(-)

diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md
index 51386a4e3..3a85aa885 100644
--- a/docs/docker_quickstart.md
+++ b/docs/docker_quickstart.md
@@ -67,21 +67,24 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
         # image: freqtradeorg/freqtrade:develop_pi
         ```
 
-=== "ARM 64 Systenms (Jetson Nano, Mac M1, Raspberry Pi 4 8GB)"
+=== "ARM 64 Systenms (Mac M1, Raspberry Pi 4, Jetson Nano)"
     In case of a Mac M1, make sure that your docker installation is running in native mode
+    Arm64 images are not yet provided via Docker Hub and need to be build locally first.
+    Depending on the device, this may take a few minutes (Apple M1) or multiple hours (Raspberry Pi)
 
     ``` bash
-    mkdir ft_userdata
-    cd ft_userdata/
-
-    # arm64 images are not yet provided via Docker Hub and need to be build locally first. Depending on the device,
-    # this may take a few minutes (Apple M1) or up to two hours (Raspberry Pi)
+    # Clone Freqtrade repository
     git clone  https://github.com/freqtrade/freqtrade.git
-    docker build -f ./freqtrade/docker/Dockerfile.aarch64 -t freqtradeorg/freqtrade:develop_arm64 freqtrade
+    cd freqtrade
+    # Optionally switch to the stable version
+    git checkout stable   
 
-    # Download the docker-compose file from the repository
-    curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
+    # Modify your docker-compose file to enable building and change the image name 
+    # (see the Note Box below for necessary changes)
 
+    # Build image
+    docker-compose build
+    
     # Create user directory structure
     docker-compose run --rm freqtrade create-userdir --userdir user_data
 
@@ -92,7 +95,10 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
     !!! Note "Change your docker Image"
         You have to change the docker image in the docker-compose file for your arm64 build to work properly.
         ``` yml
-        image: freqtradeorg/freqtrade:develop_arm64
+        image: freqtradeorg/freqtrade:custom_arm64
+        build:
+          context: .
+          dockerfile: "./docker/Dockerfile.aarch64"
         ```
 
 The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.

From 1055862bc0d691196c564e405a142ef258b03486 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 21:15:01 +0200
Subject: [PATCH 0434/1386] Extract data-load + dump from hyperopt

(Reduces memory-usage as the dataframes go out of scope)
---
 freqtrade/optimize/hyperopt.py | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 422879451..534da34fc 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -345,12 +345,7 @@ class Hyperopt:
     def _set_random_state(self, random_state: Optional[int]) -> int:
         return random_state or random.randint(1, 2**16 - 1)
 
-    def start(self) -> None:
-        self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
-        logger.info(f"Using optimizer random state: {self.random_state}")
-        self.hyperopt_table_header = -1
-        # Initialize spaces ...
-        self.init_spaces()
+    def prepare_hyperopt_data(self) -> None:
         data, timerange = self.backtesting.load_bt_data()
         logger.info("Dataload complete. Calculating indicators")
         preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
@@ -367,6 +362,15 @@ class Hyperopt:
 
         dump(preprocessed, self.data_pickle_file)
 
+    def start(self) -> None:
+        self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
+        logger.info(f"Using optimizer random state: {self.random_state}")
+        self.hyperopt_table_header = -1
+        # Initialize spaces ...
+        self.init_spaces()
+
+        self.prepare_hyperopt_data()
+
         # We don't need exchange instance anymore while running hyperopt
         self.backtesting.exchange.close()
         self.backtesting.exchange._api = None  # type: ignore

From ecee42f5615b87560b54b19049dd0662bb80eec9 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 13 May 2021 20:13:04 +0200
Subject: [PATCH 0435/1386] Read pickle file in mmap mode

---
 freqtrade/optimize/hyperopt.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 534da34fc..4f8c0ea8b 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -270,7 +270,7 @@ class Hyperopt:
             self.backtesting.strategy.trailing_only_offset_is_reached = \
                 d['trailing_only_offset_is_reached']
 
-        processed = load(self.data_pickle_file)
+        processed = load(self.data_pickle_file, mmap_mode='r+')
 
         bt_results = self.backtesting.backtest(
             processed=processed,

From 09756e300799ff8e6115ba6ac2d358c211d5835d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 14 May 2021 06:36:18 +0200
Subject: [PATCH 0436/1386] Subplots should always be included in responses

---
 freqtrade/rpc/api_server/api_schemas.py | 2 +-
 freqtrade/rpc/rpc.py                    | 4 +++-
 tests/rpc/test_rpc_apiserver.py         | 8 ++++++++
 3 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py
index e582f6aa8..c324f8828 100644
--- a/freqtrade/rpc/api_server/api_schemas.py
+++ b/freqtrade/rpc/api_server/api_schemas.py
@@ -268,7 +268,7 @@ class DeleteTrade(BaseModel):
 
 class PlotConfig_(BaseModel):
     main_plot: Dict[str, Any]
-    subplots: Optional[Dict[str, Any]]
+    subplots: Dict[str, Any]
 
 
 class PlotConfig(BaseModel):
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index fd97ad7d4..d7564bc65 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -845,5 +845,7 @@ class RPC:
                                               df_analyzed, arrow.Arrow.utcnow().datetime)
 
     def _rpc_plot_config(self) -> Dict[str, Any]:
-
+        if (self._freqtrade.strategy.plot_config and
+                'subplots' not in self._freqtrade.strategy.plot_config):
+            self._freqtrade.strategy.plot_config['subplots'] = {}
         return self._freqtrade.strategy.plot_config
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index 69d312e65..fc0dee14b 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -1142,6 +1142,14 @@ def test_api_plot_config(botclient):
     assert_response(rc)
     assert rc.json() == ftbot.strategy.plot_config
     assert isinstance(rc.json()['main_plot'], dict)
+    assert isinstance(rc.json()['subplots'], dict)
+
+    ftbot.strategy.plot_config = {'main_plot': {'sma': {}}}
+    rc = client_get(client, f"{BASE_URI}/plot_config")
+    assert_response(rc)
+
+    assert isinstance(rc.json()['main_plot'], dict)
+    assert isinstance(rc.json()['subplots'], dict)
 
 
 def test_api_strategies(botclient):

From 4bc018a4568c2dbd200875cba61fdd1534004190 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 14 May 2021 07:18:10 +0200
Subject: [PATCH 0437/1386] Change rate back to "open" for custom_sell

closes #4920
---
 freqtrade/strategy/interface.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index 7483abf6d..e2cde52eb 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -573,6 +573,10 @@ class IStrategy(ABC, HyperStrategyMixin):
 
         sell_signal = SellType.NONE
         custom_reason = ''
+        # use provided rate in backtesting, not high/low.
+        current_rate = rate
+        current_profit = trade.calc_profit_ratio(current_rate)
+
         if (ask_strategy.get('sell_profit_only', False)
                 and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
             # sell_profit_only and profit doesn't reach the offset - ignore sell signal

From 09b6923e5042983a348aee55547e830d9f6927f8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 14 May 2021 07:22:51 +0200
Subject: [PATCH 0438/1386] Use "choose" link for new issues

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 6fd59b62d..ab9597a77 100644
--- a/README.md
+++ b/README.md
@@ -154,7 +154,7 @@ You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/
 If you discover a bug in the bot, please
 [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
 first. If it hasn't been reported, please
-[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and
+[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and
 ensure you follow the template guide so that our team can assist you as
 quickly as possible.
 
@@ -163,7 +163,7 @@ quickly as possible.
 Have you a great idea to improve the bot you want to share? Please,
 first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
 If it hasn't been requested, please
-[create a new request](https://github.com/freqtrade/freqtrade/issues/new)
+[create a new request](https://github.com/freqtrade/freqtrade/issues/new/choose)
 and ensure you follow the template guide so that it does not get lost
 in the bug reports.
 

From 330fb538a9890100acbdba59a637fde616467e92 Mon Sep 17 00:00:00 2001
From: Rokas Kupstys <19151258+rokups@users.noreply.github.com>
Date: Fri, 14 May 2021 10:43:48 +0300
Subject: [PATCH 0439/1386] Couple tweaks for docs.

---
 docs/strategy-advanced.md | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index d533d81cd..417218bc3 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -62,7 +62,7 @@ class AwesomeStrategy(IStrategy):
 
         # In dry/live runs trade open date will not match candle open date therefore it must be 
         # rounded.
-        trade_date = timeframe_to_prev_date(trade.open_date_utc)
+        trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
         # Look up trade candle.
         trade_candle = dataframe.loc[dataframe['date'] == trade_date]
         # trade_candle may be None for trades that just opened as it is still incomplete.
@@ -91,8 +91,6 @@ Using custom_sell() signals in place of stoplosses though *is not recommended*.
 An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day:
 
 ``` python
-from freqtrade.strategy import IStrategy, timeframe_to_prev_date
-
 class AwesomeStrategy(IStrategy):
     def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                     current_profit: float, **kwargs):

From 5e73195b304de01f92ee841468c9d419f877f93a Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 07:01:32 +0200
Subject: [PATCH 0440/1386] Use linux lineseperator at all times

---
 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 4f8c0ea8b..3d156cccd 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -5,7 +5,6 @@ This module contains the hyperopt logic
 """
 
 import logging
-import os
 import random
 import warnings
 from datetime import datetime, timezone
@@ -164,7 +163,7 @@ class Hyperopt:
         """
         with self.results_file.open('a') as f:
             rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE)
-            f.write(os.linesep)
+            f.write("\n")
 
         self.num_epochs_saved += 1
         logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "

From 0ace35bf3d2dc9071093b830f56944fe5f5e207f Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 08:14:50 +0200
Subject: [PATCH 0441/1386] Fix unreferenced error

---
 freqtrade/rpc/rpc.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index d7564bc65..2c54d743e 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -178,7 +178,7 @@ class RPC:
                     current_rate = trade.close_rate
                 current_profit = trade.calc_profit_ratio(current_rate)
                 current_profit_abs = trade.calc_profit(current_rate)
-
+                current_profit_fiat: Optional[float] = None
                 # Calculate fiat profit
                 if self._fiat_converter:
                     current_profit_fiat = self._fiat_converter.convert_amount(

From 2eac23a15f7c3b1d032fa19bd233da0840fcbf30 Mon Sep 17 00:00:00 2001
From: Brook Miles 
Date: Sat, 15 May 2021 15:38:51 +0900
Subject: [PATCH 0442/1386] if stoploss price is above the candle high, set it
 to candle open instead.  this can occur if stoploss had previously been
 reached but the sell was prevented by `confirm_trade_exit`

---
 freqtrade/optimize/backtesting.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index e057d8189..689fb9516 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -218,6 +218,12 @@ class Backtesting:
         """
         # Special handling if high or low hit STOP_LOSS or ROI
         if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
+            if trade.stop_loss > sell_row[HIGH_IDX]:
+                # our stoploss was already higher than candle high,
+                # possibly due to a cancelled trade exit.
+                # sell at open price.
+                return sell_row[OPEN_IDX]
+
             # Set close_rate to stoploss
             return trade.stop_loss
         elif sell.sell_type == (SellType.ROI):

From e1447f955cc1ce0b90439175b935eb7a65a33148 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 10:50:00 +0200
Subject: [PATCH 0443/1386] /locks should always respond, even if there's no
 locks

closes #4942
---
 freqtrade/rpc/telegram.py      | 3 +++
 tests/rpc/test_rpc_telegram.py | 5 +++++
 2 files changed, 8 insertions(+)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index c619559de..8aa96f7b1 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -751,6 +751,9 @@ class Telegram(RPCHandler):
         Returns the currently active locks
         """
         rpc_locks = self._rpc._rpc_locks()
+        if not rpc_locks['locks']:
+            self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
+
         for locks in chunks(rpc_locks['locks'], 25):
             message = tabulate([[
                 lock['id'],
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 6d42a6845..37b5045f8 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -969,6 +969,11 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
     )
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
     patch_get_signal(freqtradebot, (True, False))
+    telegram._locks(update=update, context=MagicMock())
+    assert msg_mock.call_count == 1
+    assert 'No active locks.' in msg_mock.call_args_list[0][0][0]
+
+    msg_mock.reset_mock()
 
     PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason')
     PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef')

From 29fed37df3e92de97cc9a0a75651207fd37c9c3a Mon Sep 17 00:00:00 2001
From: Rokas Kupstys 
Date: Thu, 13 May 2021 09:47:28 +0300
Subject: [PATCH 0444/1386] Fix exception when few pairs with no data do not
 result in aborting backtest.

Exception is triggered by backtesting 20210301-20210501 range with BAKE/USDT pair (binance). Pair data starts on 2021-04-30 12:00:00 and after adjusting for startup candles pair dataframe is empty.

Solution: Since there are other pairs with enough data - skip pairs with no data and issue a warning.

Exception:
```
Traceback (most recent call last):
  File "/home/rk/src/freqtrade/freqtrade/main.py", line 37, in main
    return_code = args['func'](args)
  File "/home/rk/src/freqtrade/freqtrade/commands/optimize_commands.py", line 53, in start_backtesting
    backtesting.start()
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 502, in start
    min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 474, in backtest_one_strategy
    results = self.backtest(
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 365, in backtest
    data: Dict = self._get_ohlcv_as_lists(processed)
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 199, in _get_ohlcv_as_lists
    pair_data.loc[:, 'buy'] = 0  # cleanup from previous run
  File "/home/rk/src/freqtrade/venv/lib/python3.9/site-packages/pandas/core/indexing.py", line 692, in __setitem__
    iloc._setitem_with_indexer(indexer, value, self.name)
  File "/home/rk/src/freqtrade/venv/lib/python3.9/site-packages/pandas/core/indexing.py", line 1587, in _setitem_with_indexer
    raise ValueError(
ValueError: cannot set a frame with no defined index and a scalar
```
---
 freqtrade/optimize/backtesting.py | 22 +++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index e057d8189..450b88f3b 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -9,7 +9,7 @@ from copy import deepcopy
 from datetime import datetime, timedelta, timezone
 from typing import Any, Dict, List, Optional, Tuple
 
-from pandas import DataFrame, NaT
+from pandas import DataFrame
 
 from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
 from freqtrade.constants import DATETIME_PRINT_FORMAT
@@ -457,13 +457,21 @@ class Backtesting:
         preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
 
         # Trim startup period from analyzed dataframe
-        for pair, df in preprocessed.items():
-            preprocessed[pair] = trim_dataframe(df, timerange,
-                                                startup_candles=self.required_startup)
-        min_date, max_date = history.get_timerange(preprocessed)
-        if min_date is NaT or max_date is NaT:
+        for pair in list(preprocessed):
+            df = preprocessed[pair]
+            df = trim_dataframe(df, timerange, startup_candles=self.required_startup)
+            if len(df) > 0:
+                preprocessed[pair] = df
+            else:
+                logger.warning(f'{pair} has no data left after adjusting for startup candles, '
+                               f'skipping.')
+                del preprocessed[pair]
+
+        if not preprocessed:
             raise OperationalException(
-                "No data left after adjusting for startup candles. ")
+                "No data left after adjusting for startup candles.")
+
+        min_date, max_date = history.get_timerange(preprocessed)
         logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
                     f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
                     f'({(max_date - min_date).days} days).')

From 2d5f465f1b4646f23244692abf0c719cc3ba1cb5 Mon Sep 17 00:00:00 2001
From: Rokas Kupstys 
Date: Thu, 13 May 2021 11:49:12 +0300
Subject: [PATCH 0445/1386] Fix protections being loaded multiple times for
 first strategy when backtesting.

---
 freqtrade/optimize/backtesting.py      |  2 --
 freqtrade/optimize/hyperopt.py         |  1 +
 tests/optimize/test_backtest_detail.py |  1 +
 tests/optimize/test_backtesting.py     | 16 ++++++++++++++++
 4 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index 450b88f3b..f05a0d88b 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -115,8 +115,6 @@ class Backtesting:
 
         # Get maximum required startup period
         self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
-        # Load one (first) strategy
-        self._set_strategy(self.strategylist[0])
 
     def __del__(self):
         LoggingMixin.show_output = True
diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 5ccf02d01..5ba242bd1 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -79,6 +79,7 @@ class Hyperopt:
             self.custom_hyperopt = HyperOptAuto(self.config)
         else:
             self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
+        self.backtesting._set_strategy(self.backtesting.strategylist[0])
         self.custom_hyperopt.strategy = self.backtesting.strategy
 
         self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py
index 7da7709c6..ca2baaf33 100644
--- a/tests/optimize/test_backtest_detail.py
+++ b/tests/optimize/test_backtest_detail.py
@@ -493,6 +493,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
     patch_exchange(mocker)
     frame = _build_backtest_dataframe(data.data)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = lambda a, m: frame
     backtesting.strategy.advise_sell = lambda a, m: frame
     caplog.set_level(logging.DEBUG)
diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py
index 2ebea564b..03a65b159 100644
--- a/tests/optimize/test_backtesting.py
+++ b/tests/optimize/test_backtesting.py
@@ -83,6 +83,7 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None:
     patch_exchange(mocker)
     config['timeframe'] = '1m'
     backtesting = Backtesting(config)
+    backtesting._set_strategy(backtesting.strategylist[0])
 
     data = load_data_test(contour, testdatadir)
     processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
@@ -106,6 +107,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
     data = trim_dictlist(data, -201)
     patch_exchange(mocker)
     backtesting = Backtesting(conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
     min_date, max_date = get_timerange(processed)
     return {
@@ -285,6 +287,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
     patch_exchange(mocker)
     get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     assert backtesting.config == default_conf
     assert backtesting.timeframe == '5m'
     assert callable(backtesting.strategy.ohlcvdata_to_dataframe)
@@ -315,11 +318,13 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
 
     fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     assert backtesting.fee == 0.1234
     assert fee_mock.call_count == 0
 
     default_conf['fee'] = 0.0
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     assert backtesting.fee == 0.0
     assert fee_mock.call_count == 0
 
@@ -330,6 +335,7 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
     data = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
                              fill_up_missing=True)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
     assert len(processed['UNITTEST/BTC']) == 102
 
@@ -361,6 +367,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
     default_conf['timerange'] = '-1510694220'
 
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.bot_loop_start = MagicMock()
     backtesting.start()
     # check the logs, that will contain the backtest result
@@ -393,6 +400,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
     default_conf['timerange'] = '20180101-20180102'
 
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     with pytest.raises(OperationalException, match='No data found. Terminating.'):
         backtesting.start()
 
@@ -465,6 +473,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
     default_conf['stake_amount'] = 'unlimited'
     default_conf['max_open_trades'] = 2
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     pair = 'UNITTEST/BTC'
     row = [
         pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
@@ -508,6 +517,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
     mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
     patch_exchange(mocker)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     pair = 'UNITTEST/BTC'
     timerange = TimeRange('date', None, 1517227800, 0)
     data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
@@ -570,6 +580,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
     mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
     patch_exchange(mocker)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
 
     # Run a backtesting for an exiting 1min timeframe
     timerange = TimeRange.parse_timerange('1510688220-1510700340')
@@ -591,6 +602,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
 def test_processed(default_conf, mocker, testdatadir) -> None:
     patch_exchange(mocker)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
 
     dict_of_tickerrows = load_data_test('raise', testdatadir)
     dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows)
@@ -661,6 +673,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
 
     backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = fun  # Override
     backtesting.strategy.advise_sell = fun  # Override
     result = backtesting.backtest(**backtest_conf)
@@ -676,6 +689,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
 
     backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = fun  # Override
     backtesting.strategy.advise_sell = fun  # Override
     result = backtesting.backtest(**backtest_conf)
@@ -689,6 +703,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
                                         pair='UNITTEST/BTC', datadir=testdatadir)
     default_conf['timeframe'] = '1m'
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = _trend_alternate  # Override
     backtesting.strategy.advise_sell = _trend_alternate  # Override
     result = backtesting.backtest(**backtest_conf)
@@ -731,6 +746,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
     default_conf['timeframe'] = '5m'
 
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = _trend_alternate_hold  # Override
     backtesting.strategy.advise_sell = _trend_alternate_hold  # Override
 

From 88da1f109b3563a0f3c59d643be9c67de8ed0b89 Mon Sep 17 00:00:00 2001
From: Brook Miles 
Date: Sat, 15 May 2021 20:15:19 +0900
Subject: [PATCH 0446/1386] fix #4412 download-data does not stop downloading
 at the specified TIMERANGE end date

---
 freqtrade/data/history/history_utils.py | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py
index 32c7ce7f9..383913f66 100644
--- a/freqtrade/data/history/history_utils.py
+++ b/freqtrade/data/history/history_utils.py
@@ -265,9 +265,13 @@ def _download_trades_history(exchange: Exchange,
     """
     try:
 
-        since = timerange.startts * 1000 if \
-            (timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift(
-                days=-new_pairs_days).float_timestamp) * 1000
+        until = None
+        if (timerange and timerange.starttype == 'date'):
+          since = timerange.startts * 1000
+          if timerange.stoptype == 'date':
+            until = timerange.stopts * 1000
+        else:
+          since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
 
         trades = data_handler.trades_load(pair)
 
@@ -295,6 +299,7 @@ def _download_trades_history(exchange: Exchange,
         # Default since_ms to 30 days if nothing is given
         new_trades = exchange.get_historic_trades(pair=pair,
                                                   since=since,
+                                                  until=until,
                                                   from_id=from_id,
                                                   )
         trades.extend(new_trades[1])

From db17b1a851f749afc0335a8579e0dc1df19c47ce Mon Sep 17 00:00:00 2001
From: Brook Miles 
Date: Sat, 15 May 2021 20:20:36 +0900
Subject: [PATCH 0447/1386] fix indentation

---
 freqtrade/data/history/history_utils.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py
index 383913f66..86e9f75e6 100644
--- a/freqtrade/data/history/history_utils.py
+++ b/freqtrade/data/history/history_utils.py
@@ -267,11 +267,11 @@ def _download_trades_history(exchange: Exchange,
 
         until = None
         if (timerange and timerange.starttype == 'date'):
-          since = timerange.startts * 1000
-          if timerange.stoptype == 'date':
-            until = timerange.stopts * 1000
+            since = timerange.startts * 1000
+            if timerange.stoptype == 'date':
+                until = timerange.stopts * 1000
         else:
-          since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
+            since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
 
         trades = data_handler.trades_load(pair)
 

From 8e987784984c60a50857efd08e8ae57566da432e Mon Sep 17 00:00:00 2001
From: JoeSchr 
Date: Sat, 15 May 2021 15:21:21 +0200
Subject: [PATCH 0448/1386] Update installation.md

Fix typo
---
 docs/installation.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/installation.md b/docs/installation.md
index 18f5f4d68..c19965a18 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -269,7 +269,7 @@ git clone https://github.com/freqtrade/freqtrade.git
 cd freqtrade      
 ```
 
-#### Freqtrade instal: Conda Environment
+#### Freqtrade install: Conda Environment
 
 Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory
 

From 2ecb42a63971908901facc9823198082c976a3ce Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 15:52:02 +0200
Subject: [PATCH 0449/1386] Improve rest-api doc config samples

---
 docs/rest-api.md | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/docs/rest-api.md b/docs/rest-api.md
index a0029a44c..b9b2b29be 100644
--- a/docs/rest-api.md
+++ b/docs/rest-api.md
@@ -71,7 +71,10 @@ If you run your bot using docker, you'll need to have the bot listen to incoming
     "api_server": {
         "enabled": true,
         "listen_ip_address": "0.0.0.0",
-        "listen_port": 8080
+        "listen_port": 8080,
+        "username": "Freqtrader",
+        "password": "SuperSecret1!",
+        //...
     },
 ```
 
@@ -106,7 +109,10 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
     "api_server": {
         "enabled": true,
         "listen_ip_address": "0.0.0.0",
-        "listen_port": 8080
+        "listen_port": 8080,
+        "username": "Freqtrader",
+        "password": "SuperSecret1!",
+        //...
     }
 }
 ```

From 6b2a38ccfb9aa3802e6ecc0061d6ee7f09110784 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 19:39:46 +0200
Subject: [PATCH 0450/1386] Add absolute Profit to apiserver

---
 freqtrade/persistence/models.py         | 8 +++++---
 freqtrade/rpc/api_server/api_schemas.py | 1 +
 tests/rpc/test_rpc_apiserver.py         | 9 ++++++---
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
index afd51366a..8d2c9d1d3 100644
--- a/freqtrade/persistence/models.py
+++ b/freqtrade/persistence/models.py
@@ -815,18 +815,20 @@ class Trade(_DECL_BASE, LocalTrade):
         pair_rates = Trade.query.with_entities(
             Trade.pair,
             func.sum(Trade.close_profit).label('profit_sum'),
+            func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
             func.count(Trade.pair).label('count')
         ).filter(Trade.is_open.is_(False))\
             .group_by(Trade.pair) \
-            .order_by(desc('profit_sum')) \
+            .order_by(desc('profit_sum_abs')) \
             .all()
         return [
             {
                 'pair': pair,
-                'profit': rate,
+                'profit': profit,
+                'profit_abs': profit_abs,
                 'count': count
             }
-            for pair, rate, count in pair_rates
+            for pair, profit, profit_abs, count in pair_rates
         ]
 
     @staticmethod
diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py
index c324f8828..4d06d3ecf 100644
--- a/freqtrade/rpc/api_server/api_schemas.py
+++ b/freqtrade/rpc/api_server/api_schemas.py
@@ -57,6 +57,7 @@ class Count(BaseModel):
 class PerformanceEntry(BaseModel):
     pair: str
     profit: float
+    profit_abs: float
     count: int
 
 
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index fc0dee14b..1a66b2e81 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -710,7 +710,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,):
     assert 'draws' in rc.json()['durations']
 
 
-def test_api_performance(botclient, mocker, ticker, fee):
+def test_api_performance(botclient, fee):
     ftbot, client = botclient
     patch_get_signal(ftbot, (True, False))
 
@@ -728,6 +728,7 @@ def test_api_performance(botclient, mocker, ticker, fee):
 
     )
     trade.close_profit = trade.calc_profit_ratio()
+    trade.close_profit_abs = trade.calc_profit()
     Trade.query.session.add(trade)
 
     trade = Trade(
@@ -743,14 +744,16 @@ def test_api_performance(botclient, mocker, ticker, fee):
         close_rate=0.391
     )
     trade.close_profit = trade.calc_profit_ratio()
+    trade.close_profit_abs = trade.calc_profit()
+
     Trade.query.session.add(trade)
     Trade.query.session.flush()
 
     rc = client_get(client, f"{BASE_URI}/performance")
     assert_response(rc)
     assert len(rc.json()) == 2
-    assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61},
-                         {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}]
+    assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_abs': 0.01872279},
+                         {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
 
 
 def test_api_status(botclient, mocker, ticker, fee, markets):

From 2d7735ba048f1eb18a2301e9824432633721d32e Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 19:49:21 +0200
Subject: [PATCH 0451/1386] Update telegram to sort performance by absolute
 performance

---
 docs/telegram-usage.md         | 10 +++++-----
 freqtrade/rpc/telegram.py      |  7 +++++--
 tests/rpc/test_rpc_telegram.py |  2 +-
 3 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index 824cb17c7..07f5fe7dd 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -262,11 +262,11 @@ Note that for this to work, `forcebuy_enable` needs to be set to true.
 
 Return the performance of each crypto-currency the bot has sold.
 > Performance:
-> 1. `RCN/BTC 57.77%`  
-> 2. `PAY/BTC 56.91%`  
-> 3. `VIB/BTC 47.07%`  
-> 4. `SALT/BTC 30.24%`  
-> 5. `STORJ/BTC 27.24%`  
+> 1. `RCN/BTC 0.003 BTC (57.77%) (1)`
+> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)`
+> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)`
+> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)`
+> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)`
 > ...  
 
 ### /balance
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 8aa96f7b1..2e288ee33 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -711,8 +711,11 @@ class Telegram(RPCHandler):
             trades = self._rpc._rpc_performance()
             output = "Performance:\n"
             for i, trade in enumerate(trades):
-                stat_line = (f"{i+1}.\t {trade['pair']}\t{trade['profit']:.2f}% "
-                             f"({trade['count']})\n")
+                stat_line = (
+                    f"{i+1}.\t {trade['pair']}\t"
+                    f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
+                    f"({trade['profit']:.2f}%) "
+                    f"({trade['count']})\n")
 
                 if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
                     self._send_msg(output, parse_mode=ParseMode.HTML)
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 37b5045f8..6008ede66 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -929,7 +929,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
     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\t6.20% (1)' 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]
 
 
 def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:

From 0b1dd0d20382f9f287dba6e28f16515edb70b02b Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 09:08:13 +0200
Subject: [PATCH 0452/1386] Use correct order_id for ftx

closes #4511
---
 freqtrade/exchange/exchange.py |  3 +++
 freqtrade/exchange/ftx.py      |  6 ++++++
 freqtrade/freqtradebot.py      |  4 ++--
 tests/exchange/test_ftx.py     | 23 +++++++++++++++++++++++
 4 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 5696724b9..474eabcf3 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -1239,6 +1239,9 @@ class Exchange:
         except ccxt.BaseError as e:
             raise OperationalException(e) from e
 
+    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
+        return order['id']
+
     @retrier
     def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
                 price: float = 1, taker_or_maker: str = 'maker') -> float:
diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py
index fdfd5a674..9009e9492 100644
--- a/freqtrade/exchange/ftx.py
+++ b/freqtrade/exchange/ftx.py
@@ -8,6 +8,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
                                   OperationalException, TemporaryError)
 from freqtrade.exchange import Exchange
 from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
+from freqtrade.misc import safe_value_fallback2
 
 
 logger = logging.getLogger(__name__)
@@ -135,3 +136,8 @@ class Ftx(Exchange):
                 f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
         except ccxt.BaseError as e:
             raise OperationalException(e) from e
+
+    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
+        if order['type'] == 'stop':
+            return safe_value_fallback2(order['info'], order, 'orderId', 'id')
+        return order['id']
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index b3379a462..f451741a2 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1427,8 +1427,8 @@ class FreqtradeBot(LoggingMixin):
         """
         fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
         """
-        trades = self.exchange.get_trades_for_order(order['id'], trade.pair,
-                                                    trade.open_date)
+        trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),
+                                                    trade.pair, trade.open_date)
 
         if len(trades) == 0:
             logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py
index 494d86e56..63d99acdf 100644
--- a/tests/exchange/test_ftx.py
+++ b/tests/exchange/test_ftx.py
@@ -157,3 +157,26 @@ def test_fetch_stoploss_order(default_conf, mocker):
                            'fetch_stoploss_order', 'fetch_orders',
                            retries=API_FETCH_ORDER_RETRY_COUNT + 1,
                            order_id='_', pair='TKN/BTC')
+
+
+def test_get_order_id(mocker, default_conf):
+    exchange = get_patched_exchange(mocker, default_conf, id='ftx')
+    order = {
+        'type': STOPLOSS_ORDERTYPE,
+        'price': 1500,
+        'id': '1111',
+        'info': {
+            'orderId': '1234'
+        }
+    }
+    assert exchange.get_order_id_conditional(order) == '1234'
+
+    order = {
+        'type': 'limit',
+        'price': 1500,
+        'id': '1111',
+        'info': {
+            'orderId': '1234'
+        }
+    }
+    assert exchange.get_order_id_conditional(order) == '1111'

From 380754b8ab83187015a5d929e30c50150063fd53 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 13:16:17 +0200
Subject: [PATCH 0453/1386] Fix typos in docstrings

---
 freqtrade/freqtradebot.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index f451741a2..4013072c9 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -630,7 +630,7 @@ class FreqtradeBot(LoggingMixin):
 
     def _notify_buy(self, trade: Trade, order_type: str) -> None:
         """
-        Sends rpc notification when a buy occured.
+        Sends rpc notification when a buy occurred.
         """
         msg = {
             'trade_id': trade.id,
@@ -652,7 +652,7 @@ class FreqtradeBot(LoggingMixin):
 
     def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
         """
-        Sends rpc notification when a buy cancel occured.
+        Sends rpc notification when a buy cancel occurred.
         """
         current_rate = self.get_buy_rate(trade.pair, False)
 
@@ -713,7 +713,7 @@ class FreqtradeBot(LoggingMixin):
             except DependencyException as exception:
                 logger.warning('Unable to sell trade %s: %s', trade.pair, exception)
 
-        # Updating wallets if any trade occured
+        # Updating wallets if any trade occurred
         if trades_closed:
             self.wallets.update()
 
@@ -932,7 +932,7 @@ class FreqtradeBot(LoggingMixin):
         :return: None
         """
         if self.exchange.stoploss_adjust(trade.stop_loss, order):
-            # we check if the update is neccesary
+            # we check if the update is necessary
             update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
             if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
                 # cancelling the current stoploss on exchange first
@@ -981,7 +981,7 @@ class FreqtradeBot(LoggingMixin):
 
     def check_handle_timedout(self) -> None:
         """
-        Check if any orders are timed out and cancel if neccessary
+        Check if any orders are timed out and cancel if necessary
         :param timeoutvalue: Number of minutes until order is considered timed out
         :return: None
         """
@@ -1230,7 +1230,7 @@ class FreqtradeBot(LoggingMixin):
 
     def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None:
         """
-        Sends rpc notification when a sell occured.
+        Sends rpc notification when a sell occurred.
         """
         profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
         profit_trade = trade.calc_profit(rate=profit_rate)
@@ -1271,7 +1271,7 @@ class FreqtradeBot(LoggingMixin):
 
     def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
         """
-        Sends rpc notification when a sell cancel occured.
+        Sends rpc notification when a sell cancel occurred.
         """
         if trade.sell_order_status == reason:
             return

From 6f389764701c23ca00f350e3f696bf9e84212fbd Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 14:15:24 +0200
Subject: [PATCH 0454/1386] Introduce cancel_stoploss_with_result

---
 freqtrade/exchange/exchange.py | 21 +++++++++++++++++++++
 freqtrade/freqtradebot.py      |  7 +++++--
 tests/test_freqtradebot.py     |  3 ++-
 tests/test_integration.py      |  2 +-
 4 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 474eabcf3..c0674cd05 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -1120,6 +1120,27 @@ class Exchange:
 
         return order
 
+    def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
+        """
+        Cancel stoploss order returning a result.
+        Creates a fake result if cancel order returns a non-usable result
+        and fetch_order does not work (certain exchanges don't return cancelled orders)
+        :param order_id: stoploss-order-id to cancel
+        :param pair: Pair corresponding to order_id
+        :param amount: Amount to use for fake response
+        :return: Result from either cancel_order if usable, or fetch_order
+        """
+        corder = self.cancel_stoploss_order(order_id, pair)
+        if self.is_cancel_order_result_suitable(corder):
+            return corder
+        try:
+            order = self.fetch_stoploss_order(order_id, pair)
+        except InvalidOrderException:
+            logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
+            order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
+
+        return order
+
     @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
     def fetch_order(self, order_id: str, pair: str) -> Dict:
         if self._config['dry_run']:
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 4013072c9..9bfc343f6 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -939,7 +939,8 @@ class FreqtradeBot(LoggingMixin):
                 logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
                             f"(orderid:{order['id']}) in order to add another one ...")
                 try:
-                    co = self.exchange.cancel_stoploss_order(order['id'], trade.pair)
+                    co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair,
+                                                                         trade.amount)
                     trade.update_order(co)
                 except InvalidOrderException:
                     logger.exception(f"Could not cancel stoploss order {order['id']} "
@@ -1172,7 +1173,9 @@ class FreqtradeBot(LoggingMixin):
         # First cancelling stoploss on exchange ...
         if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
             try:
-                self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair)
+                co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id,
+                                                                     trade.pair, trade.amount)
+                trade.update_order(co)
             except InvalidOrderException:
                 logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
 
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 785e866ae..df0715111 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1371,7 +1371,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
     }
     mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order',
                  side_effect=InvalidOrderException())
-    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
+    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
+                 return_value=stoploss_order_hanging)
     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
 
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 217910961..33b3e1140 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -63,7 +63,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
         fetch_stoploss_order=stoploss_order_mock,
-        cancel_stoploss_order=cancel_order_mock,
+        cancel_stoploss_order_with_result=cancel_order_mock,
     )
 
     mocker.patch.multiple(

From 8f8d5dbff5175a8c411d90fb39d255733afc49a4 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 14:31:53 +0200
Subject: [PATCH 0455/1386] Add tests for sl_order_with_result

---
 tests/exchange/test_exchange.py | 41 +++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index 573f41bda..54ffabd4f 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -2084,6 +2084,47 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
                            order_id='_', pair='TKN/BTC')
 
 
+@pytest.mark.parametrize("exchange_name", EXCHANGES)
+def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
+    default_conf['dry_run'] = False
+    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123})
+    mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', return_value={'for': 123})
+    exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+
+    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
+                 return_value={'fee': {}, 'status': 'canceled', 'amount': 1234})
+    mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
+                 return_value={'fee': {}, 'status': 'canceled', 'amount': 1234})
+    co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
+    assert co == {'fee': {}, 'status': 'canceled', 'amount': 1234}
+
+    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
+                 return_value='canceled')
+    mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
+                 return_value='canceled')
+    # Fall back to fetch_stoploss_order
+    co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
+    assert co == {'for': 123}
+
+    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
+                 side_effect=InvalidOrderException(""))
+    mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order',
+                 side_effect=InvalidOrderException(""))
+
+    co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
+    assert co['amount'] == 555
+    assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}}
+
+    with pytest.raises(InvalidOrderException):
+        mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
+                     side_effect=InvalidOrderException("Did not find order"))
+        mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
+                     side_effect=InvalidOrderException("Did not find order"))
+        exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+        exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
+
+
+
 @pytest.mark.parametrize("exchange_name", EXCHANGES)
 def test_fetch_order(default_conf, mocker, exchange_name):
     default_conf['dry_run'] = True

From c9ac67e985914bb3700c3401eeb95eb1f437cba0 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 14:50:25 +0200
Subject: [PATCH 0456/1386] Fix some typos

---
 freqtrade/exchange/exchange.py  | 4 ++--
 freqtrade/freqtradebot.py       | 8 ++++----
 tests/exchange/test_exchange.py | 1 -
 3 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index c0674cd05..93d8f7584 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -466,7 +466,7 @@ class Exchange:
     def amount_to_precision(self, pair: str, amount: float) -> float:
         '''
         Returns the amount to buy or sell to a precision the Exchange accepts
-        Reimplementation of ccxt internal methods - ensuring we can test the result is correct
+        Re-implementation of ccxt internal methods - ensuring we can test the result is correct
         based on our definitions.
         '''
         if self.markets[pair]['precision']['amount']:
@@ -480,7 +480,7 @@ class Exchange:
     def price_to_precision(self, pair: str, price: float) -> float:
         '''
         Returns the price rounded up to the precision the Exchange accepts.
-        Partial Reimplementation of ccxt internal method decimal_to_precision(),
+        Partial Re-implementation of ccxt internal method decimal_to_precision(),
         which does not support rounding up
         TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
         align with amount_to_precision().
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 9bfc343f6..0bef29dd4 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -267,7 +267,7 @@ class FreqtradeBot(LoggingMixin):
     def update_closed_trades_without_assigned_fees(self):
         """
         Update closed trades without close fees assigned.
-        Only acts when Orders are in the database, otherwise the last orderid is unknown.
+        Only acts when Orders are in the database, otherwise the last order-id is unknown.
         """
         if self.config['dry_run']:
             # Updating open orders in dry-run does not make sense and will fail.
@@ -1223,7 +1223,7 @@ class FreqtradeBot(LoggingMixin):
             self.update_trade_state(trade, trade.open_order_id, order)
         Trade.query.session.flush()
 
-        # Lock pair for one candle to prevent immediate rebuys
+        # Lock pair for one candle to prevent immediate re-buys
         self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
                                 reason='Auto lock')
 
@@ -1327,7 +1327,7 @@ class FreqtradeBot(LoggingMixin):
         Handles closing both buy and sell orders.
         :param trade: Trade object of the trade we're analyzing
         :param order_id: Order-id of the order we're analyzing
-        :param action_order: Already aquired order object
+        :param action_order: Already acquired order object
         :return: True if order has been cancelled without being filled partially, False otherwise
         """
         if not order_id:
@@ -1397,7 +1397,7 @@ class FreqtradeBot(LoggingMixin):
     def get_real_amount(self, trade: Trade, order: Dict) -> float:
         """
         Detect and update trade fee.
-        Calls trade.update_fee() uppon correct detection.
+        Calls trade.update_fee() upon correct detection.
         Returns modified amount if the fee was taken from the destination currency.
         Necessary for exchanges which charge fees in base currency (e.g. binance)
         :return: identical (or new) amount for the trade
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index 54ffabd4f..b6b395802 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -2124,7 +2124,6 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
         exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
 
 
-
 @pytest.mark.parametrize("exchange_name", EXCHANGES)
 def test_fetch_order(default_conf, mocker, exchange_name):
     default_conf['dry_run'] = True

From 0d50e995634dac4e6e3d2a3fbbbcc776671a3618 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 19:35:30 +0200
Subject: [PATCH 0457/1386] Fix Agefilter checking for > instead of >=

---
 freqtrade/plugins/pairlist/AgeFilter.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py
index 837e99f2a..7c18460cb 100644
--- a/freqtrade/plugins/pairlist/AgeFilter.py
+++ b/freqtrade/plugins/pairlist/AgeFilter.py
@@ -86,7 +86,7 @@ class AgeFilter(IPairList):
             return True
 
         if daily_candles is not None:
-            if len(daily_candles) > self._min_days_listed:
+            if len(daily_candles) >= self._min_days_listed:
                 # We have fetched at least the minimum required number of daily candles
                 # Add to cache, store the time we last checked this symbol
                 self._symbolsChecked[pair] = int(arrow.utcnow().float_timestamp) * 1000

From 37b71b8cfd49500b794cd75fa56e5c9019a6c3c9 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 19:55:13 +0200
Subject: [PATCH 0458/1386] Fix PerformanceFilter failing in test-pairlist mode

---
 freqtrade/plugins/pairlist/PerformanceFilter.py |  7 ++++++-
 tests/plugins/test_pairlist.py                  | 15 ++++++++++++++-
 2 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py
index 73a9436fa..bf474cb21 100644
--- a/freqtrade/plugins/pairlist/PerformanceFilter.py
+++ b/freqtrade/plugins/pairlist/PerformanceFilter.py
@@ -39,7 +39,12 @@ class PerformanceFilter(IPairList):
         :return: new allowlist
         """
         # Get the trading performance for pairs from database
-        performance = pd.DataFrame(Trade.get_overall_performance())
+        try:
+            performance = pd.DataFrame(Trade.get_overall_performance())
+        except AttributeError:
+            # Performancefilter does not work in backtesting.
+            self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
+            return pairlist
 
         # Skip performance-based sorting if no performance data is available
         if len(performance) == 0:
diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py
index 8b060c287..8347687da 100644
--- a/tests/plugins/test_pairlist.py
+++ b/tests/plugins/test_pairlist.py
@@ -7,10 +7,11 @@ import pytest
 
 from freqtrade.constants import AVAILABLE_PAIRLISTS
 from freqtrade.exceptions import OperationalException
+from freqtrade.persistence import Trade
 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
 from freqtrade.plugins.pairlistmanager import PairListManager
 from freqtrade.resolvers import PairListResolver
-from tests.conftest import get_patched_freqtradebot, log_has, log_has_re
+from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re
 
 
 @pytest.fixture(scope="function")
@@ -512,6 +513,18 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
         PairListManager(MagicMock, whitelist_conf)
 
 
+def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
+    whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
+    if hasattr(Trade, 'query'):
+        del Trade.query
+    mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
+    exchange = get_patched_exchange(mocker, whitelist_conf)
+    pm = PairListManager(exchange, whitelist_conf)
+    pm.refresh_pairlist()
+
+    assert log_has("PerformanceFilter is not available in this mode.", caplog)
+
+
 def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
     default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
 

From b0f854af9507db9a55c6922aab339c53bc4f5b97 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:02 +0000
Subject: [PATCH 0459/1386] Bump ccxt from 1.49.73 to 1.50.6

Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.49.73 to 1.50.6.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
- [Commits](https://github.com/ccxt/ccxt/compare/1.49.73...1.50.6)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 3c50fa586..54f308df5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 numpy==1.20.2
 pandas==1.2.4
 
-ccxt==1.49.73
+ccxt==1.50.6
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==3.4.7
 aiohttp==3.7.4.post0

From 78c77cca737bd3cb468aecfecae3db53afff19ef Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:11 +0000
Subject: [PATCH 0460/1386] Bump pytest-cov from 2.11.1 to 2.12.0

Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.11.1 to 2.12.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.11.1...v2.12.0)

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 be7ee6857..e85120b3c 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -10,7 +10,7 @@ flake8-tidy-imports==4.2.1
 mypy==0.812
 pytest==6.2.4
 pytest-asyncio==0.15.1
-pytest-cov==2.11.1
+pytest-cov==2.12.0
 pytest-mock==3.6.1
 pytest-random-order==1.0.4
 isort==5.8.0

From 439ef197bc65e055208780a7bd088ff8814447d8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:18 +0000
Subject: [PATCH 0461/1386] Bump flake8-tidy-imports from 4.2.1 to 4.3.0

Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.2.1 to 4.3.0.
- [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases)
- [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/main/HISTORY.rst)
- [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.2.1...4.3.0)

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 be7ee6857..1fb8e6e7d 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -6,7 +6,7 @@
 coveralls==3.0.1
 flake8==3.9.2
 flake8-type-annotations==0.1.0
-flake8-tidy-imports==4.2.1
+flake8-tidy-imports==4.3.0
 mypy==0.812
 pytest==6.2.4
 pytest-asyncio==0.15.1

From 976a026d3b027a0bb5a0bc106a4a5db4f14702ac Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:31 +0000
Subject: [PATCH 0462/1386] Bump sqlalchemy from 1.4.14 to 1.4.15

Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.14 to 1.4.15.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 3c50fa586..f35138c30 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,7 @@ ccxt==1.49.73
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==3.4.7
 aiohttp==3.7.4.post0
-SQLAlchemy==1.4.14
+SQLAlchemy==1.4.15
 python-telegram-bot==13.5
 arrow==1.1.0
 cachetools==4.2.2

From 8143e6385307087d2b1d2a5facb6e9d4c320cbc3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:36 +0000
Subject: [PATCH 0463/1386] Bump aiofiles from 0.6.0 to 0.7.0

Bumps [aiofiles](https://github.com/Tinche/aiofiles) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/Tinche/aiofiles/releases)
- [Commits](https://github.com/Tinche/aiofiles/compare/v0.6.0...v0.7.0)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 3c50fa586..2987db0b0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -34,7 +34,7 @@ sdnotify==0.3.2
 fastapi==0.64.0
 uvicorn==0.13.4
 pyjwt==2.1.0
-aiofiles==0.6.0
+aiofiles==0.7.0
 
 # Support for colorized terminal output
 colorama==0.4.4

From c0b61282fbe4fb03217d7243af1487250324b045 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:52 +0000
Subject: [PATCH 0464/1386] Bump jinja2 from 2.11.3 to 3.0.0

Bumps [jinja2](https://github.com/pallets/jinja) from 2.11.3 to 3.0.0.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/2.11.3...3.0.0)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 3c50fa586..a3f818228 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,7 +17,7 @@ TA-Lib==0.4.19
 technical==1.3.0
 tabulate==0.8.9
 pycoingecko==2.0.0
-jinja2==2.11.3
+jinja2==3.0.0
 tables==3.6.1
 blosc==1.10.2
 

From 40ae21f3a8871bda3978d4a40934bff52e082f98 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 07:59:36 +0000
Subject: [PATCH 0465/1386] Bump numpy from 1.20.2 to 1.20.3

Bumps [numpy](https://github.com/numpy/numpy) from 1.20.2 to 1.20.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt)
- [Commits](https://github.com/numpy/numpy/compare/v1.20.2...v1.20.3)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index a9dce82c3..bd8c5e65d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-numpy==1.20.2
+numpy==1.20.3
 pandas==1.2.4
 
 ccxt==1.50.6

From 10ef0f54ac7847e34fc4f096a50b63144bafb42e Mon Sep 17 00:00:00 2001
From: Eugene Schava 
Date: Mon, 17 May 2021 11:12:11 +0300
Subject: [PATCH 0466/1386] Total row for telegram "/status table" command

---
 freqtrade/rpc/telegram.py | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 2e288ee33..157a2b9a5 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -5,6 +5,7 @@ This module manage Telegram communication
 """
 import json
 import logging
+import re
 from datetime import timedelta
 from html import escape
 from itertools import chain
@@ -354,19 +355,33 @@ class Telegram(RPCHandler):
         :return: None
         """
         try:
+            fiat_currency = self._config.get('fiat_display_currency', '')
             statlist, head = self._rpc._rpc_status_table(
-                self._config['stake_currency'], self._config.get('fiat_display_currency', ''))
+                self._config['stake_currency'], fiat_currency)
 
+            show_total = fiat_currency != ''
+            total_sum = 0
+            if show_total:
+                total_sum = sum(map(lambda m: float(m[1]) if m else 0, map(lambda trade: re.compile(".*\((-?\d*\.\d*)\)").match(trade[-1]), statlist)))
             max_trades_per_msg = 50
             """
             Calculate the number of messages of 50 trades per message
             0.99 is used to make sure that there are no extra (empty) messages
             As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message
             """
-            for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)):
+            messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1)
+            for i in range(0, messages_count):
+                if show_total and i == messages_count - 1:
+                    # append total line
+                    trades.append(["Total", "", "", f"{total_sum:.2f} {fiat_currency}"])
+
                 message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg],
                                    headers=head,
                                    tablefmt='simple')
+                if show_total and i == messages_count - 1:
+                    # insert separators line between Total
+                    lines = message.split("\n")
+                    message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
                 self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) From 860a4d239065132760947d0da7e507e462292943 Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Mon, 17 May 2021 11:40:57 +0200 Subject: [PATCH 0467/1386] update doc to reflect better empty dataframe check --- docs/strategy-advanced.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 417218bc3..f5f706cd6 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -65,9 +65,10 @@ class AwesomeStrategy(IStrategy): trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) # Look up trade candle. trade_candle = dataframe.loc[dataframe['date'] == trade_date] - # trade_candle may be None for trades that just opened as it is still incomplete. - if trade_candle is not None: + # trade_candle may be empty for trades that just opened as it is still incomplete. + if trade_candle.empty: # <...> + trade_candle = trade_candle.squeeze() ``` !!! Warning "Using .iloc[-1]" From 0abb9cfe2854ba02d02a048e74ee09667f5846a2 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 12:41:44 +0300 Subject: [PATCH 0468/1386] Total row for telegram "/status table" command --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 157a2b9a5..d4a79e860 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -371,11 +371,12 @@ class Telegram(RPCHandler): """ messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1) for i in range(0, messages_count): + trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg] if show_total and i == messages_count - 1: # append total line trades.append(["Total", "", "", f"{total_sum:.2f} {fiat_currency}"]) - message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], + message = tabulate(trades, headers=head, tablefmt='simple') if show_total and i == messages_count - 1: From d7479fda1fbc12bd1e48c40e1f4d0a4e0d79f22f Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 12:53:57 +0300 Subject: [PATCH 0469/1386] Total row for telegram "/status table" command fix compiler warnings --- freqtrade/rpc/telegram.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d4a79e860..11b84de19 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -362,7 +362,11 @@ class Telegram(RPCHandler): show_total = fiat_currency != '' total_sum = 0 if show_total: - total_sum = sum(map(lambda m: float(m[1]) if m else 0, map(lambda trade: re.compile(".*\((-?\d*\.\d*)\)").match(trade[-1]), statlist))) + total_sum = sum( + map(lambda m: float(m[1]) if m else 0, + map(lambda trade: re.compile(".*\\((-?\\d*\\.\\d*)\\)").match(trade[-1]), + statlist)) + ) max_trades_per_msg = 50 """ Calculate the number of messages of 50 trades per message From 915ff7e1bf1887795b4a561d737c9b2f56b2eba8 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 13:03:20 +0300 Subject: [PATCH 0470/1386] Total row for telegram "/status table" command fix mypy warnings --- 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 11b84de19..6a12488d1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -363,7 +363,7 @@ class Telegram(RPCHandler): total_sum = 0 if show_total: total_sum = sum( - map(lambda m: float(m[1]) if m else 0, + map(lambda m: float(m[1]) if m else 0.0, map(lambda trade: re.compile(".*\\((-?\\d*\\.\\d*)\\)").match(trade[-1]), statlist)) ) From 196fde44e070274dca17b2c219c790100d8ec691 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 14:45:54 +0300 Subject: [PATCH 0471/1386] Total row for telegram "/status table" command work around mypy warning --- freqtrade/rpc/telegram.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6a12488d1..1e0fad8fd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -360,13 +360,14 @@ class Telegram(RPCHandler): self._config['stake_currency'], fiat_currency) show_total = fiat_currency != '' - total_sum = 0 + total_sum = 0.0 if show_total: - total_sum = sum( - map(lambda m: float(m[1]) if m else 0.0, - map(lambda trade: re.compile(".*\\((-?\\d*\\.\\d*)\\)").match(trade[-1]), - statlist)) - ) + r = re.compile(".*\\((-?\\d*\\.\\d*)\\)") + for trade in statlist: + m = r.match(trade[-1]) + if m is not None: + total_sum += float(m[1]) + max_trades_per_msg = 50 """ Calculate the number of messages of 50 trades per message From cb50298bfec480ca9df5fa437033deeeac357c7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 May 2021 12:05:13 +0000 Subject: [PATCH 0472/1386] Bump fastapi from 0.64.0 to 0.65.1 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.64.0 to 0.65.1. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.64.0...0.65.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 762d2f491..f55cc0bdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ python-rapidjson==1.0 sdnotify==0.3.2 # API Server -fastapi==0.64.0 +fastapi==0.65.1 uvicorn==0.13.4 pyjwt==2.1.0 aiofiles==0.7.0 From 3ad8fa2f38cdd28322b2839e53674a17b66a610e Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 15:59:03 +0300 Subject: [PATCH 0473/1386] Total row for telegram "/status table" command moved sum calculation to API --- freqtrade/rpc/rpc.py | 6 ++++-- freqtrade/rpc/telegram.py | 16 ++++------------ tests/rpc/test_rpc.py | 9 ++++++--- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2c54d743e..9b0ef2ad0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -220,12 +220,13 @@ class RPC: return results def _rpc_status_table(self, stake_currency: str, - fiat_display_currency: str) -> Tuple[List, List]: + fiat_display_currency: str) -> Tuple[List, List, float]: trades = Trade.get_open_trades() if not trades: raise RPCException('no active trade') else: trades_list = [] + fiat_profit_sum = NAN for trade in trades: # calculate profit and send message to user try: @@ -243,6 +244,7 @@ class RPC: ) if fiat_profit and not isnan(fiat_profit): profit_str += f" ({fiat_profit:.2f})" + fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) else fiat_profit_sum + fiat_profit trades_list.append([ trade.id, trade.pair + ('*' if (trade.open_order_id is not None @@ -256,7 +258,7 @@ class RPC: profitcol += " (" + fiat_display_currency + ")" columns = ['ID', 'Pair', 'Since', profitcol] - return trades_list, columns + return trades_list, columns, fiat_profit_sum def _rpc_daily_profit( self, timescale: int, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1e0fad8fd..bdb1590a5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,10 +5,10 @@ This module manage Telegram communication """ import json import logging -import re from datetime import timedelta from html import escape from itertools import chain +from math import isnan from typing import Any, Callable, Dict, List, Union import arrow @@ -356,18 +356,10 @@ class Telegram(RPCHandler): """ try: fiat_currency = self._config.get('fiat_display_currency', '') - statlist, head = self._rpc._rpc_status_table( + statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( self._config['stake_currency'], fiat_currency) - show_total = fiat_currency != '' - total_sum = 0.0 - if show_total: - r = re.compile(".*\\((-?\\d*\\.\\d*)\\)") - for trade in statlist: - m = r.match(trade[-1]) - if m is not None: - total_sum += float(m[1]) - + show_total = not isnan(fiat_profit_sum) and len(statlist) > 1 max_trades_per_msg = 50 """ Calculate the number of messages of 50 trades per message @@ -379,7 +371,7 @@ class Telegram(RPCHandler): trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg] if show_total and i == messages_count - 1: # append total line - trades.append(["Total", "", "", f"{total_sum:.2f} {fiat_currency}"]) + trades.append(["Total", "", "", f"{fiat_profit_sum:.2f} {fiat_currency}"]) message = tabulate(trades, headers=head, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 6d31e7635..0ae214615 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -199,28 +199,31 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: freqtradebot.enter_positions() - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers assert "Pair" in headers assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '-0.41%' == result[0][3] + assert isnan(fiat_profit_sum) # Test with fiatconvert rpc._fiat_converter = CryptoToFiatConverter() - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers assert "Pair" in headers assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '-0.41% (-0.06)' == result[0][3] + assert -0.06 == fiat_profit_sum mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert 'nan%' == result[0][3] + assert isnan(fiat_profit_sum) def test_rpc_daily_profit(default_conf, update, ticker, fee, From 459fae6d80608f45b5f34dcfb61a0de388035d5f Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 16:22:48 +0300 Subject: [PATCH 0474/1386] Total row for telegram "/status table" command fixes --- freqtrade/rpc/rpc.py | 3 ++- tests/rpc/test_rpc.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b0ef2ad0..3f26619a9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -244,7 +244,8 @@ class RPC: ) if fiat_profit and not isnan(fiat_profit): profit_str += f" ({fiat_profit:.2f})" - fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) else fiat_profit_sum + fiat_profit + fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ + else fiat_profit_sum + fiat_profit trades_list.append([ trade.id, trade.pair + ('*' if (trade.open_order_id is not None diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0ae214615..b005fb105 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -215,7 +215,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '-0.41% (-0.06)' == result[0][3] - assert -0.06 == fiat_profit_sum + assert '-0.06' == f'{fiat_profit_sum:.2f}' mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) From 30063963982ec6ea5e56ad0fbb3783f43d4f1713 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 May 2021 20:34:02 +0200 Subject: [PATCH 0475/1386] Fix docstring typo --- freqtrade/plugins/pairlist/AgeFilter.py | 2 +- freqtrade/plugins/pairlist/IPairList.py | 2 +- freqtrade/plugins/pairlist/PrecisionFilter.py | 2 +- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- freqtrade/plugins/pairlist/SpreadFilter.py | 2 +- freqtrade/plugins/pairlist/VolatilityFilter.py | 2 +- freqtrade/plugins/pairlist/rangestabilityfilter.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 7c18460cb..8f623b062 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -78,7 +78,7 @@ class AgeFilter(IPairList): """ Validate age for the ticker :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 3e6252fc4..01eec7e90 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -68,7 +68,7 @@ class IPairList(LoggingMixin, ABC): filter_pairlist() method. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ raise NotImplementedError() diff --git a/freqtrade/plugins/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py index 519337f29..a3c262e8c 100644 --- a/freqtrade/plugins/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -48,7 +48,7 @@ class PrecisionFilter(IPairList): Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very low value pairs. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ stop_price = ticker['ask'] * self._stoploss diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index a0579b196..7f2fa4444 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -61,7 +61,7 @@ class PriceFilter(IPairList): """ Check if if one price-step (pip) is > than a certain barrier. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ if ticker.get('last', None) is None or ticker.get('last') == 0: diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 9fa211750..1b152774b 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -40,7 +40,7 @@ class SpreadFilter(IPairList): """ Validate spread for the ticker :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ if 'bid' in ticker and 'ask' in ticker and ticker['ask']: diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 400b1577d..bc617a1db 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -90,7 +90,7 @@ class VolatilityFilter(IPairList): """ Validate trading range :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 6565e92c1..8be61166b 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -83,7 +83,7 @@ class RangeStabilityFilter(IPairList): """ Validate trading range :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache From 369f19df6be9fa0141953a2ababa39e6c7447862 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 May 2021 06:47:49 +0200 Subject: [PATCH 0476/1386] Add valuefilter to Pricefilters --- docs/includes/pairlists.md | 6 ++++ freqtrade/plugins/pairlist/IPairList.py | 6 ++-- freqtrade/plugins/pairlist/PriceFilter.py | 36 +++++++++++++++++++++-- tests/plugins/test_pairlist.py | 8 +++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 20883c825..ce0cc6e57 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -112,6 +112,7 @@ The `PriceFilter` allows filtering of pairs by price. Currently the following pr * `min_price` * `max_price` +* `max_value` * `low_price_ratio` The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. @@ -120,6 +121,11 @@ This option is disabled by default, and will only apply if set to > 0. The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. This option is disabled by default, and will only apply if set to > 0. +The `max_value` setting removes pairs where the minimum value change is above a specified value. +This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20$) as the coin has risen sharply since the last limit adaption. +As a result of the above, you can only buy for 20$, or 40$ - but not for 25$. +On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit. + The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to > 0. diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 01eec7e90..74348b1a7 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -7,7 +7,7 @@ from copy import deepcopy from typing import Any, Dict, List from freqtrade.exceptions import OperationalException -from freqtrade.exchange import market_is_active +from freqtrade.exchange import Exchange, market_is_active from freqtrade.mixins import LoggingMixin @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) class IPairList(LoggingMixin, ABC): - def __init__(self, exchange, pairlistmanager, + def __init__(self, exchange: Exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], pairlist_pos: int) -> None: """ @@ -28,7 +28,7 @@ class IPairList(LoggingMixin, ABC): """ self._enabled = True - self._exchange = exchange + self._exchange: Exchange = exchange self._pairlistmanager = pairlistmanager self._config = config self._pairlistconfig = pairlistconfig diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index 7f2fa4444..f25ab8fd0 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -27,9 +27,13 @@ class PriceFilter(IPairList): self._max_price = pairlistconfig.get('max_price', 0) if self._max_price < 0: raise OperationalException("PriceFilter requires max_price to be >= 0") + self._max_value = pairlistconfig.get('max_value', 0) + if self._max_value < 0: + raise OperationalException("PriceFilter requires max_value to be >= 0") self._enabled = ((self._low_price_ratio > 0) or (self._min_price > 0) or - (self._max_price > 0)) + (self._max_price > 0) or + (self._max_value)) @property def needstickers(self) -> bool: @@ -51,6 +55,8 @@ class PriceFilter(IPairList): active_price_filters.append(f"below {self._min_price:.8f}") if self._max_price != 0: active_price_filters.append(f"above {self._max_price:.8f}") + if self._max_value != 0: + active_price_filters.append(f"Value above {self._max_value:.8f}") if len(active_price_filters): return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}." @@ -79,6 +85,32 @@ class PriceFilter(IPairList): f"because 1 unit is {changeperc * 100:.3f}%", logger.info) return False + # Perform low_amount check + if self._max_value != 0: + price = ticker['last'] + market = self._exchange.markets[pair] + limits = market['limits'] + if ('amount' in limits and 'min' in limits['amount'] + and limits['amount']['min'] is not None): + min_amount = limits['amount']['min'] + min_precision = market['precision']['amount'] + + min_value = min_amount * price + if self._exchange.precisionMode == 4: + # tick size + next_value = (min_amount + min_precision) * price + else: + # Decimal places + min_precision = pow(0.1, min_precision) + next_value = (min_amount + min_precision) * price + diff = next_value - min_value + + if diff > self._max_value: + self.log_once(f"Removed {pair} from whitelist, " + f"because min value change of {diff} > {self._max_value}) ", + logger.info) + return False + # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: @@ -89,7 +121,7 @@ class PriceFilter(IPairList): # Perform max_price check. if self._max_price != 0: if ticker['last'] > self._max_price: - self.log_once(f"Removed {ticker['symbol']} from whitelist, " + self.log_once(f"Removed {pair} from whitelist, " f"because last price > {self._max_price:.8f}", logger.info) return False diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 8347687da..552f47679 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -800,6 +800,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]", None ), + ({"method": "PriceFilter", "max_value": 0.00002000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced Value above 0.00002000.'}]", + None + ), ({"method": "PriceFilter"}, "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]", None @@ -816,6 +820,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo None, "PriceFilter requires max_price to be >= 0" ), # OperationalException expected + ({"method": "PriceFilter", "max_value": -1.00010000}, + None, + "PriceFilter requires max_value to be >= 0" + ), # OperationalException expected ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " "0.01 over the last days.'}]", From 6659a070796d3619e80951269ee3c99e5ebb39a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 May 2021 06:47:58 +0200 Subject: [PATCH 0477/1386] Add tests for max-value filter --- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- tests/plugins/test_pairlist.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index f25ab8fd0..ece9f54a8 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -33,7 +33,7 @@ class PriceFilter(IPairList): self._enabled = ((self._low_price_ratio > 0) or (self._min_price > 0) or (self._max_price > 0) or - (self._max_value)) + (self._max_value > 0)) @property def needstickers(self) -> bool: diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 552f47679..5e2274ce3 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -407,6 +407,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), + ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "max_value": 0.000001}], + "USDT", ['NANO/USDT']), ([{"method": "StaticPairList"}, {"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01, "refresh_period": 1440}], @@ -489,6 +492,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t r'because last price < .*%$', caplog) or log_has_re(r'^Removed .* from whitelist, ' r'because last price > .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because min value change of .*', caplog) or log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " r"is empty.*", caplog)) if pairlist['method'] == 'VolumePairList': From 6aa574fa2b0095f64c946955397b4d7c152fabc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 May 2021 20:58:50 +0200 Subject: [PATCH 0478/1386] Convert ROI result to proper json object closes #4952 --- freqtrade/optimize/hyperopt.py | 3 ++- tests/optimize/test_hyperopt.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 45f6bfb88..d28373c2f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -185,7 +185,8 @@ class Hyperopt: if HyperoptTools.has_space(self.config, 'sell'): result['sell'] = {p.name: params.get(p.name) for p in self.sell_space} if HyperoptTools.has_space(self.config, 'roi'): - result['roi'] = self.custom_hyperopt.generate_roi_table(params) + result['roi'] = {str(k): v for k, v in + self.custom_hyperopt.generate_roi_table(params).items()} if HyperoptTools.has_space(self.config, 'stoploss'): result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} if HyperoptTools.has_space(self.config, 'trailing'): diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a0c475efb..f23d5bc9e 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -649,10 +649,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'rsi-enabled': False, 'rsi-value': 0, 'trigger': 'macd_cross_signal'}, - 'roi': {0: 0.12000000000000001, - 20.0: 0.02, - 50.0: 0.01, - 110.0: 0}, + 'roi': {"0": 0.12000000000000001, + "20.0": 0.02, + "50.0": 0.01, + "110.0": 0}, 'sell': {'sell-adx-enabled': False, 'sell-adx-value': 0, 'sell-fastd-enabled': True, From 36eba0f110a2551bfdfb2c96045e93a770fcdeaa Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 May 2021 21:05:48 +0200 Subject: [PATCH 0479/1386] Don't use "r+" memmap, but "r2 --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d28373c2f..11a1a36f2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -271,7 +271,7 @@ class Hyperopt: self.backtesting.strategy.trailing_only_offset_is_reached = \ d['trailing_only_offset_is_reached'] - processed = load(self.data_pickle_file, mmap_mode='r+') + processed = load(self.data_pickle_file, mmap_mode='r') bt_results = self.backtesting.backtest( processed=processed, From 16c22c7b68527c6376ef7e03b5fad0a6efedc34c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 19:16:25 +0200 Subject: [PATCH 0480/1386] Add pair name to stoploss helps debugging #4972 --- freqtrade/edge/edge_positioning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 0449d6ebe..4bc0d660b 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -209,7 +209,7 @@ class Edge: if pair in self._cached_pairs: return self._cached_pairs[pair].stoploss else: - logger.warning('tried to access stoploss of a non-existing pair, ' + logger.warning(f'Tried to access stoploss of non-existing pair {pair}, ' 'strategy stoploss is returned instead.') return self.strategy.stoploss From 2565f91bc2956070c1e45ef0b18f022adbd86041 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 19:33:17 +0200 Subject: [PATCH 0481/1386] Adjust tests to reflect new stoploss behaviour --- tests/optimize/test_backtest_detail.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 7da7709c6..9d88d166a 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -185,7 +185,7 @@ tc11 = BTContainer(data=[ [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5050, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], - [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [3, 5000, 5150, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, @@ -440,6 +440,23 @@ tc27 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)] ) +# Test 28: trailing_stop should raise so candle 3 causes a stoploss +# Same case than tc11 - but candle 3 "gaps down" - the stoploss will be above the candle, +# therefore "open" will be used +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 +tc28 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.03, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + TESTS = [ tc0, tc1, @@ -469,6 +486,7 @@ TESTS = [ tc25, tc26, tc27, + tc28, ] From 7a9853bfe1a094b655bc2923e230d92b81d51efa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 20:39:55 +0200 Subject: [PATCH 0482/1386] Fix "Too many open Files" exception --- freqtrade/optimize/hyperopt.py | 4 ++-- tests/optimize/test_hyperopt.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 11a1a36f2..326ae144c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -271,8 +271,8 @@ class Hyperopt: self.backtesting.strategy.trailing_only_offset_is_reached = \ d['trailing_only_offset_is_reached'] - processed = load(self.data_pickle_file, mmap_mode='r') - + with self.data_pickle_file.open('rb') as f: + processed = load(f, mmap_mode='r') bt_results = self.backtesting.backtest( processed=processed, start_date=self.min_date, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index f23d5bc9e..b80ede734 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -600,6 +600,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: mocker.patch('freqtrade.optimize.hyperopt.get_timerange', return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) patch_exchange(mocker) + mocker.patch.object(Path, 'open') mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) optimizer_param = { From 75f88b466a6c29b64bb270adecc7cdd7dc039b81 Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 18 May 2021 19:30:36 -0400 Subject: [PATCH 0483/1386] add ability to choose unit in unfilled timeout --- docs/configuration.md | 5 +++-- freqtrade/constants.py | 4 +++- freqtrade/freqtradebot.py | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 37395c5ee..eff8fe322 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -68,8 +68,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) -| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). | `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean diff --git a/freqtrade/constants.py b/freqtrade/constants.py index bfd1e72f1..5ec60eb59 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,6 +11,7 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec +TIMEOUT_UNITS = ['minutes', 'seconds'] DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -137,7 +138,8 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'buy': {'type': 'number', 'minimum': 1}, - 'sell': {'type': 'number', 'minimum': 1} + 'sell': {'type': 'number', 'minimum': 1}, + 'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'} } }, 'bid_strategy': { diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0bef29dd4..fa372e6e8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -974,8 +974,9 @@ class FreqtradeBot(LoggingMixin): timeout = self.config.get('unfilledtimeout', {}).get(side) ordertime = arrow.get(order['datetime']).datetime if timeout is not None: - timeout_threshold = arrow.utcnow().shift(minutes=-timeout).datetime - + timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') + timeout_kwargs = {timeout_unit: -timeout} + timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime return (order['status'] == 'open' and order['side'] == side and ordertime < timeout_threshold) return False From 082fb11bbedde8a9521f72c1018a1f2ba73357fc Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 01:54:48 +0700 Subject: [PATCH 0484/1386] Avoid having error `cannot set a frame with no defined index and a scalar` --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c6b05c8c6..e6172282b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -191,8 +191,8 @@ class Backtesting: data: Dict = {} # Create dict with data for pair, pair_data in processed.items(): - pair_data.loc[:, 'buy'] = 0 # cleanup from previous run - pair_data.loc[:, 'sell'] = 0 # cleanup from previous run + pair_data['buy'] = 0 # cleanup from previous run + pair_data['sell'] = 0 # cleanup from previous run df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() From e9841910e9f6670bcca5aa41952b806590bf91cd Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Thu, 20 May 2021 00:33:33 +0300 Subject: [PATCH 0485/1386] day/week options for Telegram '/profit' command --- README.md | 2 +- docs/telegram-usage.md | 4 ++-- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/rpc/telegram.py | 17 +++++++++++++---- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ab9597a77..ada31b42b 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor - `/stop`: Stops the trader. - `/stopbuy`: Stop entering new trades. - `/status |[table]`: Lists all or specific open trades. -- `/profit`: Lists cumulative profit from all finished trades +- `/profit [day]|[week]`: Lists cumulative profit from all finished trades - `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/performance`: Show performance of each finished trade grouped by pair - `/balance`: Show account balance per currency. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 07f5fe7dd..c477921de 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -130,7 +130,7 @@ You can create your own keyboard in `config.json`: !!! Note "Supported Commands" Only the following commands are allowed. Command arguments are not supported! - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/profit day`, `/profit week`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` ## Telegram commands @@ -154,7 +154,7 @@ official commands. You can ask at any moment for help with `/help`. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). -| `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance +| `/profit [day]|[week]` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `/forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2c54d743e..7a09c1b8f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -352,9 +352,10 @@ class RPC: return {'sell_reasons': sell_reasons, 'durations': durations} def _rpc_trade_statistics( - self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: + self, stake_currency: str, fiat_display_currency: str, + start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]: """ Returns cumulative profit statistics """ - trades = Trade.get_trades().order_by(Trade.id).all() + trades = Trade.get_trades([Trade.open_date >= start_date]).order_by(Trade.id).all() profit_all_coin = [] profit_all_ratio = [] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2e288ee33..ee7493938 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,7 +5,7 @@ This module manage Telegram communication """ import json import logging -from datetime import timedelta +from datetime import datetime, timedelta from html import escape from itertools import chain from typing import Any, Callable, Dict, List, Union @@ -98,7 +98,8 @@ class Telegram(RPCHandler): # this needs refacoring of the whole telegram module (same # problem in _help()). valid_keys: List[str] = ['/start', '/stop', '/status', '/status table', - '/trades', '/profit', '/performance', '/daily', + '/trades', '/performance', '/daily', + '/profit', '/profit day', '/profit week', '/stats', '/count', '/locks', '/balance', '/stopbuy', '/reload_config', '/show_config', '/logs', '/whitelist', '/blacklist', '/edge', @@ -421,9 +422,17 @@ class Telegram(RPCHandler): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') + start_date = datetime.fromtimestamp(0) + if context.args: + if 'day' in context.args: + start_date = datetime.utcnow().date() + elif 'week' in context.args: + start_date = datetime.utcnow().date() - timedelta(days=7) + stats = self._rpc._rpc_trade_statistics( stake_cur, - fiat_disp_cur) + fiat_disp_cur, + start_date) profit_closed_coin = stats['profit_closed_coin'] profit_closed_percent_mean = stats['profit_closed_percent_mean'] profit_closed_percent_sum = stats['profit_closed_percent_sum'] @@ -901,7 +910,7 @@ class Telegram(RPCHandler): " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" - "*/profit:* `Lists cumulative profit from all finished trades`\n" + "*/profit [day]|[week]:* `Lists cumulative profit from all finished trades`\n" "*/forcesell |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" From 935ed36433608a2beb3dabc15274171a3eb93d29 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Thu, 20 May 2021 01:10:22 +0300 Subject: [PATCH 0486/1386] day/week options for Telegram '/profit' command mypy fix --- freqtrade/rpc/telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ee7493938..ebd1eacb2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,7 +5,7 @@ This module manage Telegram communication """ import json import logging -from datetime import datetime, timedelta +from datetime import datetime, date, timedelta from html import escape from itertools import chain from typing import Any, Callable, Dict, List, Union @@ -425,9 +425,9 @@ class Telegram(RPCHandler): start_date = datetime.fromtimestamp(0) if context.args: if 'day' in context.args: - start_date = datetime.utcnow().date() + start_date = datetime.combine(date.today(), datetime.min.time()) elif 'week' in context.args: - start_date = datetime.utcnow().date() - timedelta(days=7) + start_date = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=7) stats = self._rpc._rpc_trade_statistics( stake_cur, From 0358b5365fa2302520a4941122f0b40fecfe757d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 May 2021 06:25:40 +0200 Subject: [PATCH 0487/1386] Add "unfilledtimeout-unit" to full config sample --- config_full.json.example | 3 ++- freqtrade/templates/base_config.json.j2 | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 973afe2c8..24d364fdf 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -23,7 +23,8 @@ "stoploss": -0.10, "unfilledtimeout": { "buy": 10, - "sell": 30 + "sell": 30, + "unit": "minutes" }, "bid_strategy": { "price_side": "bid", diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 42f088f9f..8933ebc6a 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -9,7 +9,8 @@ "cancel_open_orders_on_exit": false, "unfilledtimeout": { "buy": 10, - "sell": 30 + "sell": 30, + "unit": "minutes" }, "bid_strategy": { "price_side": "bid", From 48210170e7a562205f459dd3568f810417c021fd Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 11:49:25 +0700 Subject: [PATCH 0488/1386] wrap with is not empty --- freqtrade/optimize/backtesting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e6172282b..955b9e614 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -191,8 +191,9 @@ class Backtesting: data: Dict = {} # Create dict with data for pair, pair_data in processed.items(): - pair_data['buy'] = 0 # cleanup from previous run - pair_data['sell'] = 0 # cleanup from previous run + if not pair_data.empty: + pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist + pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() From 1b3bfb2e7fcb21914857cc103d4873377e3dd7d2 Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 11:50:15 +0700 Subject: [PATCH 0489/1386] found root cause. --- freqtrade/optimize/hyperopt.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 326ae144c..0250935b9 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -349,19 +349,25 @@ class Hyperopt: def prepare_hyperopt_data(self) -> None: data, timerange = self.backtesting.load_bt_data() logger.info("Dataload complete. Calculating indicators") - preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) + processed: Dict[str, DataFrame] = {} + preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange, - startup_candles=self.backtesting.required_startup) - self.min_date, self.max_date = get_timerange(preprocessed) + trimed_df = trim_dataframe(df, timerange, + startup_candles=self.backtesting.required_startup) + if not trimed_df.empty: + processed[pair] = trimed_df + else: + logger.warn(f'Pair {pair} got removed because triming dataframe left nothing') + + self.min_date, self.max_date = get_timerange(processed) logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(self.max_date - self.min_date).days} days)..') - dump(preprocessed, self.data_pickle_file) + dump(processed, self.data_pickle_file) def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) From c2b9da68e14d96c1b012170a7de69be83e28352f Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 11:56:11 +0700 Subject: [PATCH 0490/1386] fix indent --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 955b9e614..0144d934b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -192,8 +192,8 @@ class Backtesting: # Create dict with data for pair, pair_data in processed.items(): if not pair_data.empty: - pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist - pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist + pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist + pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() From 6172e67fcd0693f292bf032acbb2383ce7b65c72 Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 11:56:31 +0700 Subject: [PATCH 0491/1386] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 0250935b9..3d4588145 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -357,9 +357,9 @@ class Hyperopt: trimed_df = trim_dataframe(df, timerange, startup_candles=self.backtesting.required_startup) if not trimed_df.empty: - processed[pair] = trimed_df + processed[pair] = trimed_df else: - logger.warn(f'Pair {pair} got removed because triming dataframe left nothing') + logger.warn(f'Pair {pair} got removed because triming dataframe left nothing') self.min_date, self.max_date = get_timerange(processed) From 336f4aa6a787817d9a33ece738dc88c0a0e43af0 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Thu, 20 May 2021 08:17:08 +0300 Subject: [PATCH 0492/1386] day/week options for Telegram '/profit' command isort fix --- 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 ebd1eacb2..19c520efa 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,7 +5,7 @@ This module manage Telegram communication """ import json import logging -from datetime import datetime, date, timedelta +from datetime import date, datetime, timedelta from html import escape from itertools import chain from typing import Any, Callable, Dict, List, Union From 0045d3a726f266f720f6229c1ae5d1c073ab99b2 Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Fri, 21 May 2021 11:18:16 +0700 Subject: [PATCH 0493/1386] fix wrong json key --- scripts/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 900b784f2..ece0a253e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -396,7 +396,7 @@ def main(args): sys.exit() config = load_config(args['config']) - url = config.get('api_server', {}).get('server_url', '127.0.0.1') + url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1') port = config.get('api_server', {}).get('listen_port', '8080') username = config.get('api_server', {}).get('username') password = config.get('api_server', {}).get('password') From 1a30e39222ae73ca32247ceccea1e881a437d6ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 08:06:27 +0200 Subject: [PATCH 0494/1386] Move squeeze into if block --- docs/strategy-advanced.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f5f706cd6..52879c7b4 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -40,7 +40,6 @@ class AwesomeStrategy(IStrategy): !!! Note If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. - ## Dataframe access You may access dataframe in various strategy functions by querying it from dataprovider. @@ -67,8 +66,8 @@ class AwesomeStrategy(IStrategy): trade_candle = dataframe.loc[dataframe['date'] == trade_date] # trade_candle may be empty for trades that just opened as it is still incomplete. if trade_candle.empty: + trade_candle = trade_candle.squeeze() # <...> - trade_candle = trade_candle.squeeze() ``` !!! Warning "Using .iloc[-1]" From f398888865c732df55b0ecee3c775aba4d4a9ff4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 08:26:19 +0200 Subject: [PATCH 0495/1386] Refactor preprocessed trimming to seperate method --- freqtrade/data/converter.py | 22 ++++++++++++++++++++++ freqtrade/optimize/backtesting.py | 12 ++---------- freqtrade/optimize/hyperopt.py | 12 +++--------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index af6c6a2ef..7098834ec 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime +from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList @@ -145,6 +146,27 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', return df +def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange: TimeRange, + startup_candles: int) -> Dict[str, DataFrame]: + """ + Trim startup period from analyzed dataframes + :param preprocessed: Dict of pair: dataframe + :param timerange: timerange (use start and end date if available) + :param startup_candles: Startup-candles that should be removed + :return: Dict of trimmed dataframes + """ + processed: Dict[str, DataFrame] = {} + + for pair, df in preprocessed.items(): + trimed_df = trim_dataframe(df, timerange, startup_candles=startup_candles) + if not trimed_df.empty: + processed[pair] = trimed_df + else: + logger.warning(f'{pair} has no data left after adjusting for startup candles, ' + f'skipping.') + return processed + + def order_book_to_dataframe(bids: list, asks: list) -> DataFrame: """ TODO: This should get a dedicated test diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0144d934b..aff09921c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -15,7 +15,7 @@ from freqtrade.configuration import TimeRange, remove_credentials, validate_conf from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe -from freqtrade.data.converter import trim_dataframe +from freqtrade.data.converter import trim_dataframes from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -462,15 +462,7 @@ class Backtesting: preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe - for pair in list(preprocessed): - df = preprocessed[pair] - df = trim_dataframe(df, timerange, startup_candles=self.required_startup) - if len(df) > 0: - preprocessed[pair] = df - else: - logger.warning(f'{pair} has no data left after adjusting for startup candles, ' - f'skipping.') - del preprocessed[pair] + preprocessed = trim_dataframes(preprocessed, timerange, self.required_startup) if not preprocessed: raise OperationalException( diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3d4588145..cf5559a24 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -20,7 +20,7 @@ from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN -from freqtrade.data.converter import trim_dataframe +from freqtrade.data.converter import trim_dataframes from freqtrade.data.history import get_timerange from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting @@ -350,16 +350,10 @@ class Hyperopt: data, timerange = self.backtesting.load_bt_data() logger.info("Dataload complete. Calculating indicators") - processed: Dict[str, DataFrame] = {} preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) + # Trim startup period from analyzed dataframe - for pair, df in preprocessed.items(): - trimed_df = trim_dataframe(df, timerange, - startup_candles=self.backtesting.required_startup) - if not trimed_df.empty: - processed[pair] = trimed_df - else: - logger.warn(f'Pair {pair} got removed because triming dataframe left nothing') + processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup) self.min_date, self.max_date = get_timerange(processed) From 96ea10e56213000a2534e7433515b3ef9045cef6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 08:52:56 +0200 Subject: [PATCH 0496/1386] Fix circular import in hyperopt --- freqtrade/data/converter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 7098834ec..ffee0c52c 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,7 +10,6 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList @@ -146,7 +145,7 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', return df -def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange: TimeRange, +def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange, startup_candles: int) -> Dict[str, DataFrame]: """ Trim startup period from analyzed dataframes From 0e6c1d28f466893d70008275de92198b2f17a446 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 09:32:18 +0200 Subject: [PATCH 0497/1386] Fix cleanup CI by updating action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4169661c6..5a0837eb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,7 +301,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch - uses: rokroskar/workflow-run-cleanup-action@v0.3.2 + 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 }}" From edcfa940937d0141f74b1bdd4f8161f76746fb83 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 15 May 2021 11:26:22 +0300 Subject: [PATCH 0498/1386] Include zero duration trades in backtesting report. --- docs/backtesting.md | 1 + freqtrade/optimize/optimize_reports.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index ee9926f32..8b3aa8cc1 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -413,6 +413,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. +- `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 2be3c3e62..b509f216f 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -208,6 +208,8 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: winning_trades = results.loc[results['profit_ratio'] > 0] draw_trades = results.loc[results['profit_ratio'] == 0] losing_trades = results.loc[results['profit_ratio'] < 0] + zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) & + (results['sell_reason'] == 'trailing_stop_loss')]) return { 'wins': len(winning_trades), @@ -219,6 +221,7 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: if not winning_trades.empty else timedelta()), 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) if not losing_trades.empty else timedelta()), + 'zero_duration_trades': zero_duration_trades, } @@ -496,6 +499,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio']) worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio']) + zero_duration_trades_percent =\ + 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] metrics = [ ('Backtesting from', strat_results['backtest_start']), ('Backtesting to', strat_results['backtest_end']), @@ -508,7 +513,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), + ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"), ('Trades per day', strat_results['trades_per_day']), ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'], strat_results['stake_currency'])), @@ -532,6 +537,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), + ('Zero Duration Trades', f"{zero_duration_trades_percent:.1f}% " + f"({strat_results['zero_duration_trades']})"), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], From e1dc1357ce37243f4f8ff214480d339b7249154c Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 15 May 2021 12:00:01 +0300 Subject: [PATCH 0499/1386] Add drawdown column to strategy summary table. --- docs/backtesting.md | 10 +++++----- freqtrade/optimize/optimize_reports.py | 18 ++++++++++++++++- tests/optimize/test_optimize_reports.py | 26 ++++++++++++++++--------- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 8b3aa8cc1..5ee0b8a49 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -473,11 +473,11 @@ There will be an additional table comparing win/losses of the different strategi Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy. ``` -=========================================================== STRATEGY SUMMARY =========================================================== -| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:| -| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | -| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | +=========================================================== STRATEGY SUMMARY ========================================================================= +| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | +|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| +| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | +| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | ``` ## Next step diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b509f216f..d8ef92e7c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -164,6 +164,17 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]: tabular_data.append(_generate_result_line( results['results'], results['config']['dry_run_wallet'], strategy) ) + try: + max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'], + value_col='profit_ratio') + max_drawdown_abs, _, _, _, _ = calculate_max_drawdown(results['results'], + value_col='profit_abs') + except ValueError: + max_drawdown_per = 0 + max_drawdown_abs = 0 + tabular_data[-1]['max_drawdown_per'] = round(max_drawdown_per * 100, 2) + tabular_data[-1]['max_drawdown_abs'] = \ + round_coin_value(max_drawdown_abs, results['config']['stake_currency'], False) return tabular_data @@ -485,10 +496,15 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: """ floatfmt = _get_line_floatfmt(stake_currency) headers = _get_line_header('Strategy', stake_currency) + # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless + # therefore we slip this column in only for strategy summary here. + headers.append(f'Drawdown {stake_currency}') + headers.append('Drawdown %') output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'], + t['max_drawdown_abs'], t['max_drawdown_per'] ] for t in strategy_results] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 1afa5c59c..e87cec6fa 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,3 +1,4 @@ +import datetime import re from datetime import timedelta from pathlib import Path @@ -325,9 +326,12 @@ def test_text_table_strategy(default_conf): default_conf['max_open_trades'] = 2 default_conf['dry_run_wallet'] = 3 results = {} + date = datetime.datetime(year=2020, month=1, day=1, hour=12, minute=30) + delta = datetime.timedelta(days=1) results['TestStrategy1'] = {'results': pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'close_date': [date, date + delta, date + delta * 2], 'profit_ratio': [0.1, 0.2, 0.3], 'profit_abs': [0.2, 0.4, 0.5], 'trade_duration': [10, 30, 10], @@ -340,6 +344,7 @@ def test_text_table_strategy(default_conf): results['TestStrategy2'] = {'results': pd.DataFrame( { 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], + 'close_date': [date, date + delta, date + delta * 2], 'profit_ratio': [0.4, 0.2, 0.3], 'profit_abs': [0.4, 0.4, 0.5], 'trade_duration': [15, 30, 15], @@ -351,18 +356,21 @@ def test_text_table_strategy(default_conf): ), 'config': default_conf} result_str = ( - '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot' - ' Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |\n' - '|---------------+--------+----------------+----------------+------------------+' - '----------------+----------------+--------+---------+----------|\n' - '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' - ' 36.67 | 0:17:00 | 3 | 0 | 0 |\n' - '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' - ' 43.33 | 0:20:00 | 3 | 0 | 0 |' + '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC ' + '| Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown BTC ' + '| Drawdown % |\n' + '|---------------+--------+----------------+----------------+------------------' + '+----------------+----------------+--------+---------+----------+----------------' + '+--------------|\n' + '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 ' + '| 36.67 | 0:17:00 | 3 | 0 | 0 | 0 ' + '| 0 |\n' + '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 ' + '| 43.33 | 0:20:00 | 3 | 0 | 0 | 0 ' + '| 0 |' ) strategy_results = generate_strategy_comparison(all_results=results) - assert text_table_strategy(strategy_results, 'BTC') == result_str From debd98ad9a6278ca026bdb7fb32f287687f994ee Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 21 May 2021 11:29:22 +0300 Subject: [PATCH 0500/1386] Make results table more compact by merging win/draw/loss columns and drawdown abs/% into single columns. --- freqtrade/optimize/optimize_reports.py | 34 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d8ef92e7c..68f315926 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -43,7 +43,7 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]: Generate floatformat (goes in line with _generate_result_line()) """ return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f', - '.2f', 'd', 'd', 'd', 'd'] + '.2f', 'd', 's', 's'] def _get_line_header(first_column: str, stake_currency: str) -> List[str]: @@ -52,7 +52,11 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: """ return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Wins', 'Draws', 'Losses'] + 'Win Draw Loss'] + + +def _generate_wins_draws_losses(wins, draws, losses): + return f'{wins:>4} {draws:>4} {losses:>4}' def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: @@ -451,7 +455,8 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st floatfmt = _get_line_floatfmt(stake_currency) output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + t['profit_total_pct'], t['duration_avg'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']) ] for t in pair_results] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, @@ -468,9 +473,7 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren headers = [ 'Sell Reason', 'Sells', - 'Wins', - 'Draws', - 'Losses', + 'Win Draws Loss', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', @@ -478,7 +481,8 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren ] output = [[ - t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], + t['sell_reason'], t['trades'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), t['profit_mean_pct'], t['profit_sum_pct'], round_coin_value(t['profit_total_abs'], stake_currency, False), t['profit_total_pct'], @@ -498,14 +502,20 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: headers = _get_line_header('Strategy', stake_currency) # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless # therefore we slip this column in only for strategy summary here. - headers.append(f'Drawdown {stake_currency}') - headers.append('Drawdown %') + headers.append(f'Drawdown') + + # Align drawdown string on the center two space separator. + drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results] + dd_pad_abs = max([len(t['max_drawdown_abs']) for t in strategy_results]) + dd_pad_per = max([len(dd) for dd in drawdown]) + drawdown = [f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency} {dd:>{dd_pad_per}}%' + for t, dd in zip(strategy_results, drawdown)] output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'], - t['max_drawdown_abs'], t['max_drawdown_per'] - ] for t in strategy_results] + t['profit_total_pct'], t['duration_avg'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown] + for t, drawdown in zip(strategy_results, drawdown)] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") From 981b2df7caec96565ba66d2abb7201326fcfaa96 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 21 May 2021 12:00:24 +0300 Subject: [PATCH 0501/1386] Include win:loss ratio in results tables. --- freqtrade/optimize/optimize_reports.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 68f315926..0474ec1bf 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -52,11 +52,17 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: """ return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Win Draw Loss'] + 'Win Draw Loss Win%'] def _generate_wins_draws_losses(wins, draws, losses): - return f'{wins:>4} {draws:>4} {losses:>4}' + if wins > 0 and losses == 0: + wl_ratio = '100' + elif wins == 0: + wl_ratio = '0' + else: + wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100' + return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}' def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: @@ -473,7 +479,7 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren headers = [ 'Sell Reason', 'Sells', - 'Win Draws Loss', + 'Win Draws Loss Win%', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', From dfa412f0de369ea8302a7c59b7fae731e08a0e2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 13:24:13 +0200 Subject: [PATCH 0502/1386] Fix typo in filter --- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index ece9f54a8..5b5afb557 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -107,7 +107,7 @@ class PriceFilter(IPairList): if diff > self._max_value: self.log_once(f"Removed {pair} from whitelist, " - f"because min value change of {diff} > {self._max_value}) ", + f"because min value change of {diff} > {self._max_value}.", logger.info) return False From 9537d9f4e2b9c3302866a2652a516a41b5ff7335 Mon Sep 17 00:00:00 2001 From: Nicolas Menescardi Date: Fri, 21 May 2021 11:27:22 -0300 Subject: [PATCH 0503/1386] Update strategy-customization.md Fix typo: 'This will method will...' -> 'This method will...' --- docs/strategy-customization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 49456c047..cfea60d22 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -159,7 +159,7 @@ Edit the method `populate_buy_trend()` in your strategy file to update your buy It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. -This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action". +This method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action". Sample from `user_data/strategies/sample_strategy.py`: @@ -193,7 +193,7 @@ Please note that the sell-signal is only used if `use_sell_signal` is set to tru It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. -This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action". +This method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action". Sample from `user_data/strategies/sample_strategy.py`: From 45e2621505fbfe748b5507dccdb697bebd5fbe34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 19:19:38 +0200 Subject: [PATCH 0504/1386] Add minimum-filled protection for buy cancels --- freqtrade/freqtradebot.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fa372e6e8..1c3a759f4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1045,6 +1045,16 @@ class FreqtradeBot(LoggingMixin): # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in ('cancelled', 'canceled', 'closed'): + filled_val = order.get('filled', 0.0) or 0.0 + filled_stake = filled_val * trade.open_rate + minstake = self.exchange.get_min_pair_stake_amount( + trade.pair, trade.open_rate, self.strategy.stoploss) + + if filled_val > 0 and filled_stake < minstake: + logger.warning( + f"Order {trade.open_order_id} for {trade.pair} not cancelled, " + f"as the filled amount of {filled_val} would result in an unsellable trade.") + return False corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) # Avoid race condition where the order could not be cancelled coz its already filled. From 4e94d3d3e5bb84425a875d298dc66dbba81ed9b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 19:32:26 +0200 Subject: [PATCH 0505/1386] Add test for too small buy check --- tests/test_freqtradebot.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index df0715111..4d9284a2f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2431,13 +2431,22 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non freqtrade._notify_buy_cancel = MagicMock() trade = MagicMock() - trade.pair = 'LTC/ETH' + trade.pair = 'LTC/USDT' + trade.open_rate = 200 limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 + cancel_order_mock.reset_mock() + caplog.clear() + limit_buy_order['filled'] = 0.01 + assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert cancel_order_mock.call_count == 0 + assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog) + + caplog.clear() cancel_order_mock.reset_mock() limit_buy_order['filled'] = 2 assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) @@ -2492,7 +2501,8 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, freqtrade._notify_buy_cancel = MagicMock() trade = MagicMock() - trade.pair = 'LTC/ETH' + trade.pair = 'LTC/USDT' + trade.open_rate = 200 limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] From 6acb2eb2b60c42a18a02b886f75c0d75da17489a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 20:35:39 +0200 Subject: [PATCH 0506/1386] Add average column to orders table --- freqtrade/persistence/migrations.py | 32 ++++++++++++++++++++++++++--- freqtrade/persistence/models.py | 2 ++ tests/test_persistence.py | 16 +++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 961363b0e..d89256baf 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -123,6 +123,27 @@ def migrate_open_orders_to_trades(engine): """) +def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List): + # Schema migration necessary + engine.execute(f"alter table orders rename to {table_back_name}") + # drop indexes on backup table + for index in inspector.get_indexes(table_back_name): + engine.execute(f"drop index {index['name']}") + + # let SQLAlchemy create the schema as required + decl_base.metadata.create_all(engine) + + engine.execute(f""" + insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, average, remaining, cost, order_date, + order_filled_date, order_update_date) + select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, null average, remaining, cost, order_date, + order_filled_date, order_update_date + from {table_back_name} + """) + + def check_migrate(engine, decl_base, previous_tables) -> None: """ Checks if migration is necessary and migrates if necessary @@ -145,6 +166,11 @@ def check_migrate(engine, decl_base, previous_tables) -> None: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) else: - pass - # Empty for now - as there is only one iteration of the orders table so far. - # table_back_name = get_backup_name(tabs, 'orders_bak') + cols_order = inspector.get_columns('orders') + + if not has_column(cols_order, 'average'): + tabs = get_table_names_for_table(inspector, 'orders') + # Empty for now - as there is only one iteration of the orders table so far. + table_back_name = get_backup_name(tabs, 'orders_bak') + + migrate_orders_table(decl_base, inspector, engine, table_back_name, cols) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8d2c9d1d3..428455ff8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -122,6 +122,7 @@ class Order(_DECL_BASE): order_type = Column(String, nullable=True) side = Column(String, nullable=True) price = Column(Float, nullable=True) + average = Column(Float, nullable=True) amount = Column(Float, nullable=True) filled = Column(Float, nullable=True) remaining = Column(Float, nullable=True) @@ -150,6 +151,7 @@ class Order(_DECL_BASE): self.price = order.get('price', self.price) self.amount = order.get('amount', self.amount) self.filled = order.get('filled', self.filled) + self.average = order.get('average', self.average) self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) if 'timestamp' in order and order['timestamp'] is not None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 8470e12c2..a38c12470 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -627,6 +627,22 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[1].order_id == 'stop_order_id222' assert orders[1].ft_order_side == 'stoploss' + caplog.clear() + # Drop latest column + engine.execute("alter table orders drop average") + # Run init to test migration + init_db(default_conf['db_url'], default_conf['dry_run']) + + assert log_has("trying orders_bak0", caplog) + + orders = Order.query.all() + assert len(orders) == 2 + assert orders[0].order_id == 'buy_order' + assert orders[0].ft_order_side == 'buy' + + assert orders[1].order_id == 'stop_order_id222' + assert orders[1].ft_order_side == 'stoploss' + def test_migrate_mid_state(mocker, default_conf, fee, caplog): """ From 41e3233bab3bc4a79b4dc595aa80d88d1c4bf8b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 20:44:11 +0200 Subject: [PATCH 0507/1386] Fix failing test --- tests/test_persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index a38c12470..38fe3ff94 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -629,7 +629,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): caplog.clear() # Drop latest column - engine.execute("alter table orders drop average") + engine.execute("alter table orders drop column average") # Run init to test migration init_db(default_conf['db_url'], default_conf['dry_run']) From a7216e627988e62c7883eef4282785c30b3b5048 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 20:53:38 +0200 Subject: [PATCH 0508/1386] SQLite does not know drop column --- tests/test_persistence.py | 48 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 38fe3ff94..2949fdfe9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock import arrow import pytest -from sqlalchemy import create_engine +from sqlalchemy import create_engine, inspect from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException @@ -629,11 +629,53 @@ def test_migrate_new(mocker, default_conf, fee, caplog): caplog.clear() # Drop latest column - engine.execute("alter table orders drop column average") + engine.execute("alter table orders rename to orders_bak") + inspector = inspect(engine) + + for index in inspector.get_indexes('orders_bak'): + engine.execute(f"drop index {index['name']}") + # Recreate table + engine.execute(""" + CREATE TABLE orders ( + id INTEGER NOT NULL, + ft_trade_id INTEGER, + ft_order_side VARCHAR NOT NULL, + ft_pair VARCHAR NOT NULL, + ft_is_open BOOLEAN NOT NULL, + order_id VARCHAR NOT NULL, + status VARCHAR, + symbol VARCHAR, + order_type VARCHAR, + side VARCHAR, + price FLOAT, + amount FLOAT, + filled FLOAT, + remaining FLOAT, + cost FLOAT, + order_date DATETIME, + order_filled_date DATETIME, + order_update_date DATETIME, + PRIMARY KEY (id), + CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), + FOREIGN KEY(ft_trade_id) REFERENCES trades (id) + ) + """ + ) + + engine.execute(""" + insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, remaining, cost, order_date, + order_filled_date, order_update_date) + select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, remaining, cost, order_date, + order_filled_date, order_update_date + from orders_bak + """) + # Run init to test migration init_db(default_conf['db_url'], default_conf['dry_run']) - assert log_has("trying orders_bak0", caplog) + assert log_has("trying orders_bak1", caplog) orders = Order.query.all() assert len(orders) == 2 From 44bbc0718e2d568c35e35344a5945a5f8971c796 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 20:54:18 +0200 Subject: [PATCH 0509/1386] CLosing bracket --- tests/test_persistence.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 2949fdfe9..669f220bb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -659,8 +659,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), FOREIGN KEY(ft_trade_id) REFERENCES trades (id) ) - """ - ) + """) engine.execute(""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, From 5285cd69b4d19bdaaeb267e0f24065a078b05787 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 10:12:03 +0200 Subject: [PATCH 0510/1386] Add documentation for Postgres and Mysql --- docs/sql_cheatsheet.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 569af33ff..477396931 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -19,7 +19,7 @@ The freqtrade docker image does contain sqlite3, so you can edit the database wi ``` bash docker-compose exec freqtrade /bin/bash -sqlite3 .sqlite +sqlite3 .sqlite ``` ## Open the DB @@ -99,3 +99,32 @@ 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. + +## Use a different database system + +!!! 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. + +### PostgreSQL + +Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems. + +Installation: +`pip install psycopg2` + +Usage: +`... --db-url postgresql+psycopg2://:@localhost:5432/` + +Freqtrade will automatically create the tables necessary upon startup. + +If you're running different instances of Freqtrade, you must either setup one database per Instance or use different users / schemas for your connections. + +### MariaDB / MySQL + +Freqtrade supports MariaDB by using SQLAlchemy, which supports multiple different database systems. + +Installation: +`pip install pymysql` + +Usage: +`... --db-url mysql+pymysql://:@localhost:3306/` From cc064f157422fe7b35b3208b25e66c34cbd10b3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 10:12:23 +0200 Subject: [PATCH 0511/1386] String columns should have a max-length defined otherwise MySql will not work. --- freqtrade/persistence/models.py | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 428455ff8..f2e7a10c4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -112,15 +112,15 @@ class Order(_DECL_BASE): trade = relationship("Trade", back_populates="orders") - ft_order_side = Column(String, nullable=False) - ft_pair = Column(String, nullable=False) + ft_order_side = Column(String(25), nullable=False) + ft_pair = Column(String(25), nullable=False) ft_is_open = Column(Boolean, nullable=False, default=True, index=True) - order_id = Column(String, nullable=False, index=True) - status = Column(String, nullable=True) - symbol = Column(String, nullable=True) - order_type = Column(String, nullable=True) - side = Column(String, nullable=True) + order_id = Column(String(255), nullable=False, index=True) + status = Column(String(255), nullable=True) + symbol = Column(String(25), nullable=True) + order_type = Column(String(50), nullable=True) + side = Column(String(25), nullable=True) price = Column(Float, nullable=True) average = Column(Float, nullable=True) amount = Column(Float, nullable=True) @@ -658,15 +658,15 @@ class Trade(_DECL_BASE, LocalTrade): orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") - exchange = Column(String, nullable=False) - pair = Column(String, nullable=False, index=True) + exchange = Column(String(25), nullable=False) + pair = Column(String(25), nullable=False, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True) fee_open = Column(Float, nullable=False, default=0.0) fee_open_cost = Column(Float, nullable=True) - fee_open_currency = Column(String, nullable=True) + fee_open_currency = Column(String(25), nullable=True) fee_close = Column(Float, nullable=False, default=0.0) fee_close_cost = Column(Float, nullable=True) - fee_close_currency = Column(String, nullable=True) + fee_close_currency = Column(String(25), nullable=True) open_rate = Column(Float) open_rate_requested = Column(Float) # open_trade_value - calculated via _calc_open_trade_value @@ -680,7 +680,7 @@ class Trade(_DECL_BASE, LocalTrade): amount_requested = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) - open_order_id = Column(String) + open_order_id = Column(String(255)) # absolute value of the stop loss stop_loss = Column(Float, nullable=True, default=0.0) # percentage value of the stop loss @@ -690,16 +690,16 @@ class Trade(_DECL_BASE, LocalTrade): # percentage value of the initial stop loss initial_stop_loss_pct = Column(Float, nullable=True) # stoploss order id which is on exchange - stoploss_order_id = Column(String, nullable=True, index=True) + stoploss_order_id = Column(String(255), nullable=True, index=True) # last update time of the stoploss order on exchange stoploss_last_update = Column(DateTime, nullable=True) # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String, nullable=True) - sell_order_status = Column(String, nullable=True) - strategy = Column(String, nullable=True) + sell_reason = Column(String(100), nullable=True) + sell_order_status = Column(String(100), nullable=True) + strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) def __init__(self, **kwargs): @@ -856,8 +856,8 @@ class PairLock(_DECL_BASE): id = Column(Integer, primary_key=True) - pair = Column(String, nullable=False, index=True) - reason = Column(String, nullable=True) + pair = Column(String(25), nullable=False, index=True) + reason = Column(String(255), nullable=True) # Time the pair was locked (start time) lock_time = Column(DateTime, nullable=False) # Time until the pair is locked (end time) From a0921ec75342f036b2f30c5a22478181f53820a0 Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Mon, 17 May 2021 11:31:19 +0200 Subject: [PATCH 0512/1386] Add backoff timer for coingecko API Set a future timestamp when we should retry getting coingecko data. This fixes conversion from stake to fiat when running multiple bots as we don't simply accept the 429 error from Coingecko but handle it. --- freqtrade/rpc/fiat_convert.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 380070deb..908f7adf0 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -4,10 +4,12 @@ e.g BTC to USD """ import logging -from typing import Dict +import datetime +from typing import Dict from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI +from requests.exceptions import RequestException from freqtrade.constants import SUPPORTED_FIAT @@ -25,6 +27,7 @@ class CryptoToFiatConverter: _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} + _backoff = int def __new__(cls): """ @@ -47,8 +50,13 @@ class CryptoToFiatConverter: def _load_cryptomap(self) -> None: try: coinlistings = self._coingekko.get_coins_list() - # Create mapping table from synbol to coingekko_id + # Create mapping table from symbol to coingekko_id self._cryptomap = {x['symbol']: x['id'] for x in coinlistings} + except RequestException as request_exception: + if "429" in str(request_exception): + logger.warning("Too many requests for Coingecko API, backing off and trying again later.") + # Set backoff timestamp to 60 seconds in the future + self._backoff = datetime.datetime.now().timestamp() + 60 except (Exception) as exception: logger.error( f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") @@ -127,6 +135,9 @@ class CryptoToFiatConverter: if crypto_symbol == fiat_symbol: return 1.0 + if self._cryptomap == {} and self._backoff <= datetime.datetime.now().timestamp(): + self._load_cryptomap() + if crypto_symbol not in self._cryptomap: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) From 8842e0d161fec054bfec2f2ec4d3e2455dd3a9ef Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Mon, 17 May 2021 12:05:25 +0200 Subject: [PATCH 0513/1386] Fix flake8 error in fiat_convert --- freqtrade/rpc/fiat_convert.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 908f7adf0..2088b0258 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -54,7 +54,8 @@ class CryptoToFiatConverter: self._cryptomap = {x['symbol']: x['id'] for x in coinlistings} except RequestException as request_exception: if "429" in str(request_exception): - logger.warning("Too many requests for Coingecko API, backing off and trying again later.") + logger.warning( + "Too many requests for Coingecko API, backing off and trying again later.") # Set backoff timestamp to 60 seconds in the future self._backoff = datetime.datetime.now().timestamp() + 60 except (Exception) as exception: From ab6bfbad12791f65f784c9cb2ba39998269d7cfb Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 11:51:43 +0200 Subject: [PATCH 0514/1386] Handle RequestExceptions that are not 429s in _load_cryptomap --- freqtrade/rpc/fiat_convert.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 2088b0258..5b73663c8 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -58,6 +58,13 @@ class CryptoToFiatConverter: "Too many requests for Coingecko API, backing off and trying again later.") # Set backoff timestamp to 60 seconds in the future self._backoff = datetime.datetime.now().timestamp() + 60 + return + # If the request is not a 429 error we want to raise the normal error + logger.error( + "Could not load FIAT Cryptocurrency map for the following problem: {}".format( + request_exception + ) + ) except (Exception) as exception: logger.error( f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") From 6e05f856b470c26e5682bf8495e34bac0e78b042 Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 11:55:03 +0200 Subject: [PATCH 0515/1386] Abort _find_price when cryptomap is empty after retry --- freqtrade/rpc/fiat_convert.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 5b73663c8..4987f8ce1 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -145,6 +145,9 @@ class CryptoToFiatConverter: if self._cryptomap == {} and self._backoff <= datetime.datetime.now().timestamp(): self._load_cryptomap() + # return 0.0 if we still dont have data to check, no reason to proceed + if self._cryptomap == {}: + return 0.0 if crypto_symbol not in self._cryptomap: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) From e4ca944597099ca7505d22f333c7759ef2971a09 Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 12:04:24 +0200 Subject: [PATCH 0516/1386] Add tests for coingecko backoff --- tests/rpc/test_fiat_convert.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 2d43addff..7f345a1a2 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock +import datetime import pytest from requests.exceptions import RequestException @@ -21,6 +22,12 @@ def test_fiat_convert_is_supported(mocker): def test_fiat_convert_find_price(mocker): fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._backoff = 0 + mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap', + return_value=None) + assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 0.0 + with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC') @@ -115,6 +122,24 @@ def test_fiat_convert_without_network(mocker): CryptoToFiatConverter._coingekko = cmc_temp +def test_fiat_too_many_requests_response(mocker, caplog): + # Because CryptoToFiatConverter is a Singleton we reset the listings + req_exception = "429 Too Many Requests" + listmock = MagicMock(return_value="{}", side_effect=RequestException(req_exception)) + mocker.patch.multiple( + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_coins_list=listmock, + ) + # with pytest.raises(RequestEsxception): + fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._load_cryptomap() + + length_cryptomap = len(fiat_convert._cryptomap) + assert length_cryptomap == 0 + assert fiat_convert._backoff > datetime.datetime.now().timestamp() + assert log_has('Too many requests for Coingecko API, backing off and trying again later.', caplog) + def test_fiat_invalid_response(mocker, caplog): # Because CryptoToFiatConverter is a Singleton we reset the listings listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}") @@ -132,7 +157,6 @@ def test_fiat_invalid_response(mocker, caplog): assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*', caplog) - def test_convert_amount(mocker): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) From 21d986710dfabbcd40fe8e4555731974cccc3188 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Sat, 22 May 2021 13:26:59 +0200 Subject: [PATCH 0517/1386] Fix missing `not` in `empty` check See discussing here: https://github.com/freqtrade/freqtrade/pull/4963#discussion_r633457596 seems that request was only partially implemented --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 52879c7b4..cb759eb2f 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -65,7 +65,7 @@ class AwesomeStrategy(IStrategy): # Look up trade candle. trade_candle = dataframe.loc[dataframe['date'] == trade_date] # trade_candle may be empty for trades that just opened as it is still incomplete. - if trade_candle.empty: + if not trade_candle.empty: trade_candle = trade_candle.squeeze() # <...> ``` From f8cdd6475cc60d15791db7e0fd6de665496f2812 Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 13:43:33 +0200 Subject: [PATCH 0518/1386] Reduce warnings when waiting for coingecko backoff --- freqtrade/rpc/fiat_convert.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 4987f8ce1..c4c5b5e25 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -143,10 +143,13 @@ class CryptoToFiatConverter: if crypto_symbol == fiat_symbol: return 1.0 - if self._cryptomap == {} and self._backoff <= datetime.datetime.now().timestamp(): - self._load_cryptomap() - # return 0.0 if we still dont have data to check, no reason to proceed - if self._cryptomap == {}: + if self._cryptomap == {}: + if self._backoff <= datetime.datetime.now().timestamp(): + self._load_cryptomap() + # return 0.0 if we still dont have data to check, no reason to proceed + if self._cryptomap == {}: + return 0.0 + else: return 0.0 if crypto_symbol not in self._cryptomap: From be1385617152a95af6764c4100782f33c6ea07ab Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 13:43:48 +0200 Subject: [PATCH 0519/1386] Fix flake8 error in test_fiat_convert --- tests/rpc/test_fiat_convert.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 7f345a1a2..e8b05024f 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -138,7 +138,11 @@ def test_fiat_too_many_requests_response(mocker, caplog): length_cryptomap = len(fiat_convert._cryptomap) assert length_cryptomap == 0 assert fiat_convert._backoff > datetime.datetime.now().timestamp() - assert log_has('Too many requests for Coingecko API, backing off and trying again later.', caplog) + assert log_has( + 'Too many requests for Coingecko API, backing off and trying again later.', + caplog + ) + def test_fiat_invalid_response(mocker, caplog): # Because CryptoToFiatConverter is a Singleton we reset the listings @@ -157,6 +161,7 @@ def test_fiat_invalid_response(mocker, caplog): assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*', caplog) + def test_convert_amount(mocker): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) From 25cc4eae96b0a6fd18047ce98da9a8aef6073b76 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 21 May 2021 12:31:16 +0300 Subject: [PATCH 0520/1386] Fix tests that broke after table formatting changed. --- freqtrade/optimize/optimize_reports.py | 2 +- tests/optimize/test_optimize_reports.py | 52 ++++++++++++------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0474ec1bf..bda531d0d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -508,7 +508,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: headers = _get_line_header('Strategy', stake_currency) # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless # therefore we slip this column in only for strategy summary here. - headers.append(f'Drawdown') + headers.append('Drawdown') # Align drawdown string on the center two space separator. drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results] diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index e87cec6fa..e0ff2f219 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -39,14 +39,14 @@ def test_text_table_bt_results(): ) result_str = ( - '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' - ' Tot Profit % | Avg Duration | Wins | Draws | Losses |\n' - '|---------+--------+----------------+----------------+------------------+' - '----------------+----------------+--------+---------+----------|\n' - '| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 |' - ' 15.00 | 0:20:00 | 2 | 0 | 0 |\n' - '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |' - ' 15.00 | 0:20:00 | 2 | 0 | 0 |' + '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |' + ' Avg Duration | Win Draw Loss Win% |\n' + '|---------+--------+----------------+----------------+------------------+----------------+' + '----------------+-------------------------|\n' + '| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 | 15.00 |' + ' 0:20:00 | 2 0 0 100 |\n' + '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 | 15.00 |' + ' 0:20:00 | 2 0 0 100 |' ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', @@ -271,14 +271,14 @@ def test_text_table_sell_reason(): ) result_str = ( - '| Sell Reason | Sells | Wins | Draws | Losses |' - ' Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |\n' - '|---------------+---------+--------+---------+----------+' - '----------------+----------------+------------------+----------------|\n' - '| roi | 2 | 2 | 0 | 0 |' - ' 15 | 30 | 0.6 | 15 |\n' - '| stop_loss | 1 | 0 | 0 | 1 |' - ' -10 | -10 | -0.2 | -5 |' + '| Sell Reason | Sells | Win Draws Loss Win% | Avg Profit % | Cum Profit % |' + ' Tot Profit BTC | Tot Profit % |\n' + '|---------------+---------+--------------------------+----------------+----------------+' + '------------------+----------------|\n' + '| roi | 2 | 2 0 0 100 | 15 | 30 |' + ' 0.6 | 15 |\n' + '| stop_loss | 1 | 0 0 1 0 | -10 | -10 |' + ' -0.2 | -5 |' ) sell_reason_stats = generate_sell_reason_stats(max_open_trades=2, @@ -356,18 +356,14 @@ def test_text_table_strategy(default_conf): ), 'config': default_conf} result_str = ( - '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC ' - '| Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown BTC ' - '| Drawdown % |\n' - '|---------------+--------+----------------+----------------+------------------' - '+----------------+----------------+--------+---------+----------+----------------' - '+--------------|\n' - '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 ' - '| 36.67 | 0:17:00 | 3 | 0 | 0 | 0 ' - '| 0 |\n' - '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 ' - '| 43.33 | 0:20:00 | 3 | 0 | 0 | 0 ' - '| 0 |' + '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' + ' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n' + '|---------------+--------+----------------+----------------+------------------+' + '----------------+----------------+-------------------------+-----------------------|\n' + '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' + ' 36.67 | 0:17:00 | 3 0 0 100 | 0.00000000 BTC 0.00% |\n' + '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' + ' 43.33 | 0:20:00 | 3 0 0 100 | 0.00000000 BTC 0.00% |' ) strategy_results = generate_strategy_comparison(all_results=results) From 08c707e0cf601210eb206820a2c03091a1317a2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 15:38:13 +0200 Subject: [PATCH 0521/1386] Update docs with new format --- docs/backtesting.md | 60 +++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 5ee0b8a49..e720206bb 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -237,29 +237,29 @@ The most important in the backtesting is to understand the result. A backtesting result will look like that: ``` -========================================================= BACKTESTING REPORT ======================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:| -| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 0 | 21 | -| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 0 | 8 | -| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 0 | 14 | -| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 0 | 7 | -| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 0 | 10 | -| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 0 | 20 | -| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 0 | 15 | -| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 0 | 17 | -| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 0 | 18 | -| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 0 | 9 | -| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 0 | 21 | -| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 0 | 7 | -| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 0 | 13 | -| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 | 0 | 5 | -| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 | 0 | 9 | -| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 0 | 11 | -| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 0 | 23 | -| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 0 | 15 | -| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | -========================================================= SELL REASON STATS ========================================================= +========================================================= BACKTESTING REPORT ========================================================== +| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | +|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| +| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | +| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | +| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | +| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | +| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | +| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | +| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | +| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | +| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | +| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | +| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | +| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | +| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | +| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | +| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | +| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | +| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | +| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | +========================================================= SELL REASON STATS ========================================================== | Sell Reason | Sells | Wins | Draws | Losses | |:-------------------|--------:|------:|-------:|--------:| | trailing_stop_loss | 205 | 150 | 0 | 55 | @@ -267,11 +267,11 @@ A backtesting result will look like that: | sell_signal | 56 | 36 | 0 | 20 | | force_sell | 2 | 0 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:| -| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 | 0 | -| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 | 0 | -| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | 0 | +| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | +|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| +| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | +| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | +| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | =============== SUMMARY METRICS =============== | Metric | Value | |-----------------------+---------------------| @@ -297,6 +297,7 @@ A backtesting result will look like that: | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Zero Duration Trades | 4.6% (20) | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -318,7 +319,7 @@ The last line will give you the overall performance of your strategy, here: ``` -| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | ``` The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has @@ -384,6 +385,7 @@ It contains some useful key metrics about performance of your strategy on backte | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Zero Duration Trades | 4.6% (20) | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | From 0693458507953787bff2599d1f0117c58049cc0a Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 16:26:58 +0200 Subject: [PATCH 0522/1386] Update freqtrade/rpc/fiat_convert.py --- freqtrade/rpc/fiat_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index c4c5b5e25..116f9b156 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -27,7 +27,7 @@ class CryptoToFiatConverter: _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} - _backoff = int + _backoff: int = 0 def __new__(cls): """ From a7bd8b0aa55a824506cfa2aa5451bb7341f10752 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 17:03:16 +0200 Subject: [PATCH 0523/1386] Fix exception in plotting when no trades where generated as seen in #4981 --- freqtrade/data/btanalysis.py | 50 +++++++++++++++++++----------------- freqtrade/plot/plotting.py | 10 +++++--- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index e07b0a40f..e7af5eab8 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -156,33 +156,35 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non data = data['strategy'][strategy]['trades'] df = pd.DataFrame(data) - df['open_date'] = pd.to_datetime(df['open_date'], - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - utc=True, - infer_datetime_format=True - ) + if not df.empty: + df['open_date'] = pd.to_datetime(df['open_date'], + utc=True, + infer_datetime_format=True + ) + df['close_date'] = pd.to_datetime(df['close_date'], + utc=True, + infer_datetime_format=True + ) else: # old format - only with lists. df = pd.DataFrame(data, columns=BT_DATA_COLUMNS_OLD) - - df['open_date'] = pd.to_datetime(df['open_date'], - unit='s', - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - unit='s', - utc=True, - infer_datetime_format=True - ) - # Create compatibility with new format - df['profit_abs'] = df['close_rate'] - df['open_rate'] - if 'profit_ratio' not in df.columns: - df['profit_ratio'] = df['profit_percent'] - df = df.sort_values("open_date").reset_index(drop=True) + if not df.empty: + df['open_date'] = pd.to_datetime(df['open_date'], + unit='s', + utc=True, + infer_datetime_format=True + ) + df['close_date'] = pd.to_datetime(df['close_date'], + unit='s', + utc=True, + infer_datetime_format=True + ) + # Create compatibility with new format + df['profit_abs'] = df['close_rate'] - df['open_rate'] + if not df.empty: + if 'profit_ratio' not in df.columns: + df['profit_ratio'] = df['profit_percent'] + df = df.sort_values("open_date").reset_index(drop=True) return df diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index d5a729ee1..bb4283406 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -77,7 +77,8 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): ) except ValueError as e: raise OperationalException(e) from e - trades = trim_dataframe(trades, timerange, 'open_date') + if not trades.empty: + trades = trim_dataframe(trades, timerange, 'open_date') return {"ohlcv": data, "trades": trades, @@ -540,8 +541,11 @@ def load_and_plot_trades(config: Dict[str, Any]): df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) df_analyzed = trim_dataframe(df_analyzed, timerange) - trades_pair = trades.loc[trades['pair'] == pair] - trades_pair = extract_trades_of_period(df_analyzed, trades_pair) + if not trades.empty: + trades_pair = trades.loc[trades['pair'] == pair] + trades_pair = extract_trades_of_period(df_analyzed, trades_pair) + else: + trades_pair = trades fig = generate_candlestick_graph( pair=pair, From 765c824bfcd913ef072ed135ee022dba6e275575 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 17:15:35 +0200 Subject: [PATCH 0524/1386] isort --- freqtrade/rpc/fiat_convert.py | 6 +++--- tests/rpc/test_fiat_convert.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 116f9b156..5ae20afa1 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -3,10 +3,10 @@ Module that define classes to convert Crypto-currency to FIAT e.g BTC to USD """ -import logging import datetime - +import logging from typing import Dict + from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI from requests.exceptions import RequestException @@ -27,7 +27,7 @@ class CryptoToFiatConverter: _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} - _backoff: int = 0 + _backoff: float = 0.0 def __new__(cls): """ diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index e8b05024f..5174f9416 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -1,9 +1,9 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, # pragma pylint: disable=protected-access, C0103 +import datetime from unittest.mock import MagicMock -import datetime import pytest from requests.exceptions import RequestException From db985cbc2ea4bfa29b2d605ca6c2c0a0d152e579 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 23 May 2021 09:24:50 +0300 Subject: [PATCH 0525/1386] Fix hyperopt-show failing to display old results with missing new fields. --- freqtrade/optimize/optimize_reports.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index bda531d0d..ce6758210 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -531,8 +531,18 @@ def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio']) worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio']) - zero_duration_trades_percent =\ - 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] + + # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show + # command stores these results and newer version of freqtrade must be able to handle old + # results with missing new fields. + zero_duration_trades = '--' + + if 'zero_duration_trades' in strat_results: + zero_duration_trades_per = \ + 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] + zero_duration_trades = f'{zero_duration_trades_per}% ' \ + f'({strat_results["zero_duration_trades"]})' + metrics = [ ('Backtesting from', strat_results['backtest_start']), ('Backtesting to', strat_results['backtest_end']), @@ -569,8 +579,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), - ('Zero Duration Trades', f"{zero_duration_trades_percent:.1f}% " - f"({strat_results['zero_duration_trades']})"), + ('Zero Duration Trades', zero_duration_trades), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], From 916ece6a29fa140398bdc637b8bcf8194e0f7614 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 09:15:36 +0200 Subject: [PATCH 0526/1386] More realistic testcase for results --- tests/optimize/test_optimize_reports.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index e0ff2f219..575bb9092 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -28,13 +28,10 @@ def test_text_table_bt_results(): results = pd.DataFrame( { - 'pair': ['ETH/BTC', 'ETH/BTC'], - 'profit_ratio': [0.1, 0.2], - 'profit_abs': [0.2, 0.4], - 'trade_duration': [10, 30], - 'wins': [2, 0], - 'draws': [0, 0], - 'losses': [0, 0] + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_ratio': [0.1, 0.2, -0.05], + 'profit_abs': [0.2, 0.4, -0.1], + 'trade_duration': [10, 30, 20], } ) @@ -43,10 +40,10 @@ def test_text_table_bt_results(): ' Avg Duration | Win Draw Loss Win% |\n' '|---------+--------+----------------+----------------+------------------+----------------+' '----------------+-------------------------|\n' - '| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 | 15.00 |' - ' 0:20:00 | 2 0 0 100 |\n' - '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 | 15.00 |' - ' 0:20:00 | 2 0 0 100 |' + '| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' + ' 0:20:00 | 2 0 1 66.7 |\n' + '| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' + ' 0:20:00 | 2 0 1 66.7 |' ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', From 7f125315b059cc529750415945366474efa5ac7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 09:36:02 +0200 Subject: [PATCH 0527/1386] Track Rejected Trades closes #3423 --- freqtrade/optimize/backtesting.py | 26 +++++++++++++++++++++----- freqtrade/optimize/optimize_reports.py | 2 ++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aff09921c..642ed2b76 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -177,6 +177,7 @@ class Backtesting: Trade.use_db = False PairLocks.reset_locks() Trade.reset_trades() + self.rejected_trades = 0 self.dataprovider.clear_cache() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: @@ -336,6 +337,17 @@ class Backtesting: trades.append(trade1) return trades + def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: + if max_open_trades <= 0: + # Always allow trades when max_open_trades is enabled. + return True + if open_trade_count < max_open_trades: + + return True + # Rejected trade + self.rejected_trades += 1 + return False + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, @@ -397,11 +409,14 @@ class Backtesting: # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row - if ((position_stacking or len(open_trades[pair]) == 0) - and (max_open_trades <= 0 or open_trade_count_start < max_open_trades) - and tmp != end_date - and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 - and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): + if ( + (position_stacking or len(open_trades[pair]) == 0) + and self.trade_slot_available(max_open_trades, open_trade_count_start) + and tmp != end_date + and row[BUY_IDX] == 1 + and row[SELL_IDX] != 1 + and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) + ): trade = self._enter_trade(pair, row) if trade: # TODO: hacky workaround to avoid opening > max_open_trades @@ -439,6 +454,7 @@ class Backtesting: 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), + 'rejected': self.rejected_trades, 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ce6758210..ddccfd1dc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -355,6 +355,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], + 'rejected_signals': content['rejected'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -561,6 +562,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), + ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " From a39860e0de12e1d3c4590b235dc07c74460e7210 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 09:46:51 +0200 Subject: [PATCH 0528/1386] Add tests for rejected signals --- docs/backtesting.md | 3 +++ freqtrade/optimize/backtesting.py | 9 +++------ freqtrade/optimize/optimize_reports.py | 5 ++--- tests/optimize/test_backtesting.py | 3 +++ tests/optimize/test_hyperopt.py | 2 ++ tests/optimize/test_optimize_reports.py | 2 ++ 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index e720206bb..2027c2079 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -298,6 +298,7 @@ A backtesting result will look like that: | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | Zero Duration Trades | 4.6% (20) | +| Rejected Buy signals | 3089 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -386,6 +387,7 @@ It contains some useful key metrics about performance of your strategy on backte | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | Zero Duration Trades | 4.6% (20) | +| Rejected Buy signals | 3089 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -416,6 +418,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results. +- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 642ed2b76..cbc0995aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -338,11 +338,8 @@ class Backtesting: return trades def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: - if max_open_trades <= 0: - # Always allow trades when max_open_trades is enabled. - return True - if open_trade_count < max_open_trades: - + # Always allow trades when max_open_trades is enabled. + if max_open_trades <= 0 or open_trade_count < max_open_trades: return True # Rejected trade self.rejected_trades += 1 @@ -454,7 +451,7 @@ class Backtesting: 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), - 'rejected': self.rejected_trades, + 'rejected_signals': self.rejected_trades, 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ddccfd1dc..090af4a85 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -355,7 +355,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], - 'rejected_signals': content['rejected'], + 'rejected_signals': content['rejected_signals'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -562,8 +562,6 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), - ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), - ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), @@ -582,6 +580,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('Zero Duration Trades', zero_duration_trades), + ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 03a65b159..632d458ce 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -831,6 +831,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', @@ -938,12 +939,14 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'results': result1, 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, }, { 'results': result2, 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, } ]) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b80ede734..d7eb8bf67 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -441,6 +441,7 @@ def test_hyperopt_format_results(hyperopt): 'config': hyperopt.config, 'locks': [], 'final_balance': 0.02, + 'rejected_signals': 2, 'backtest_start_time': 1619718665, 'backtest_end_time': 1619718665, } @@ -593,6 +594,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: }), 'config': hyperopt_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, } diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 575bb9092..f9dac3397 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -79,6 +79,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): 'config': default_conf, 'locks': [], 'final_balance': 1000.02, + 'rejected_signals': 20, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } @@ -126,6 +127,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): 'config': default_conf, 'locks': [], 'final_balance': 1000.02, + 'rejected_signals': 20, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } From 3f956441fcb4c6d011c1ac95c8f9e4fb249fb235 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 15:53:54 +0200 Subject: [PATCH 0529/1386] Properly format % of zero_duration_trades --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ce6758210..a76bb2e3a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -540,7 +540,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: if 'zero_duration_trades' in strat_results: zero_duration_trades_per = \ 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] - zero_duration_trades = f'{zero_duration_trades_per}% ' \ + zero_duration_trades = f'{zero_duration_trades_per:.2f}% ' \ f'({strat_results["zero_duration_trades"]})' metrics = [ From 77302ea178896aaba9f2608557a4c6bbb796e7c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 16:01:49 +0200 Subject: [PATCH 0530/1386] Update documentation for forcebuy query --- docs/assets/telegram_forcebuy.png | Bin 0 -> 17895 bytes docs/telegram-usage.md | 9 +++++++-- tests/rpc/test_rpc_telegram.py | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 docs/assets/telegram_forcebuy.png diff --git a/docs/assets/telegram_forcebuy.png b/docs/assets/telegram_forcebuy.png new file mode 100644 index 0000000000000000000000000000000000000000..b0592bff3571dab298a1f951f1aaa8eae255a011 GIT binary patch literal 17895 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 **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` -### /forcebuy +### /forcebuy [rate] > **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) +Omitting the pair will open a query asking for the pair to buy (based on the current whitelist). + +![Telegram force-buy screenshot](assets/telegram_forcebuy.png) + Note that for this to work, `forcebuy_enable` needs to be set to true. [More details](configuration.md#understand-forcebuy_enable) @@ -254,7 +258,8 @@ Note that for this to work, `forcebuy_enable` needs to be set to true. ### /performance Return the performance of each crypto-currency the bot has sold. -> Performance: + +> Performance: > 1. `RCN/BTC 57.77%` > 2. `PAY/BTC 56.91%` > 3. `VIB/BTC 47.07%` diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a3147f956..6e818ff8d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -930,6 +930,11 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: assert inline_msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' keyboard = inline_msg_mock.call_args_list[0][1]['keyboard'] assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 + update = MagicMock() + update.callback_query = MagicMock() + update.callback_query.data = 'XRP/USDT' + telegram._forcebuy_inline(update, None) + assert fbuy_mock.call_count == 1 def test_performance_handle(default_conf, update, ticker, fee, From 6f990c5976549c9abd76a240cd3730125efca724 Mon Sep 17 00:00:00 2001 From: Priveyes <3041616+Pascal66@users.noreply.github.com> Date: Sun, 23 May 2021 18:49:07 +0200 Subject: [PATCH 0531/1386] Fix a rare error in save_result : ValueError: Out of range float values are not JSON compliant freqtrade/freqtrade/optimize/hyperopt.py", line 166, in _save_result rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE) ValueError: Out of range float values are not JSON compliant --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cf5559a24..cba82cee6 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -163,7 +163,7 @@ class Hyperopt: :param epoch: result dictionary for this epoch. """ with self.results_file.open('a') as f: - rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE) + rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN) f.write("\n") self.num_epochs_saved += 1 From 4c02e6667f2a80568e829a29c5a50dc52fbdbdb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 05:23:55 +0000 Subject: [PATCH 0532/1386] Bump mkdocs-material from 7.1.4 to 7.1.5 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.4 to 7.1.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.4...7.1.5) 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 3d469fde7..89011272d 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.4 +mkdocs-material==7.1.5 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 From 2fddb4ae43ef81372755136b5f99ce6db350b4f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 05:24:02 +0000 Subject: [PATCH 0533/1386] Bump jinja2 from 3.0.0 to 3.0.1 Bumps [jinja2](https://github.com/pallets/jinja) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.0.0...3.0.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f55cc0bdc..0d293800e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ TA-Lib==0.4.19 technical==1.3.0 tabulate==0.8.9 pycoingecko==2.0.0 -jinja2==3.0.0 +jinja2==3.0.1 tables==3.6.1 blosc==1.10.2 From 7757c476fdeb125b46e45b754841ccb8380f4b5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 05:24:08 +0000 Subject: [PATCH 0534/1386] Bump ta-lib from 0.4.19 to 0.4.20 Bumps [ta-lib](https://github.com/mrjbq7/ta-lib) from 0.4.19 to 0.4.20. - [Release notes](https://github.com/mrjbq7/ta-lib/releases) - [Changelog](https://github.com/mrjbq7/ta-lib/blob/master/CHANGELOG) - [Commits](https://github.com/mrjbq7/ta-lib/compare/TA_Lib-0.4.19...TA_Lib-0.4.20) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f55cc0bdc..6280a1e9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ requests==2.25.1 urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 -TA-Lib==0.4.19 +TA-Lib==0.4.20 technical==1.3.0 tabulate==0.8.9 pycoingecko==2.0.0 From 20ccda1699acf4ac6b592811f845ce34438eeab8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 05:24:17 +0000 Subject: [PATCH 0535/1386] Bump ccxt from 1.50.6 to 1.50.30 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.50.6 to 1.50.30. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.50.6...1.50.30) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f55cc0bdc..363caf3b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.3 pandas==1.2.4 -ccxt==1.50.6 +ccxt==1.50.30 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From af16614bf29a6f3235c780c5f9746541e6f66cab Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 May 2021 07:48:36 +0200 Subject: [PATCH 0536/1386] Fix formatting issue --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cba82cee6..85bcbb8e3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -163,7 +163,8 @@ class Hyperopt: :param epoch: result dictionary for this epoch. """ with self.results_file.open('a') as f: - rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN) + rapidjson.dump(epoch, f, default=str, + number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN) f.write("\n") self.num_epochs_saved += 1 From ba3997185bc72ca6e30fb89c6c090007c41f850c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 May 2021 08:03:33 +0200 Subject: [PATCH 0537/1386] Update wheels for ta-lib --- .../TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl | Bin 482390 -> 0 bytes .../TA_Lib-0.4.19-cp38-cp38-win_amd64.whl | Bin 504687 -> 0 bytes .../TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl | Bin 0 -> 482853 bytes .../TA_Lib-0.4.20-cp38-cp38-win_amd64.whl | Bin 0 -> 505119 bytes build_helpers/install_windows.ps1 | 5 ++--- docs/windows_installation.md | 4 +++- 6 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl delete mode 100644 build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl create mode 100644 build_helpers/TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl create mode 100644 build_helpers/TA_Lib-0.4.20-cp38-cp38-win_amd64.whl diff --git a/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl deleted file mode 100644 index 5adbda8a7e81d37f3c1d387baba2457bc7da5b8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 482390 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 z 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%0DmI|B^Ur#glPP7( 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%0D`LhoO>< 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

d5>^1clM71`yiuah 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
kOM+?hL&ACy}ZxlJNB*( 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 Oc{PsjG+)Ow*#85I*)doE diff --git a/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl deleted file mode 100644 index b652c7ee042df991c4820569f660cd4ca04c7bf5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 504687 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 M09=V6&=7T#bp;_A!3;nVRZF z7!*`CPR)bLtWNdp_%MT$=#O+1*TUgxE~IcEaBk-c?8HYD`puBS8m(S{Fj9>kZ21(6 z`y;%(r|TmEq3Qm#jb=XI5LqL{wTTSgb z+)4?W{?-r0G}Db{W(RzlAi*$_mX2Rdjd&0}DIqZ)M-Iho3IaEcQ=_5Vmq!S1{hi$tfQ2xp0iVh!tno*MvLtZ83|5-N&I(v_M@cvi)�I> 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

0{$ zy6ZwNU}fs^R>bV%f=y`#x+lG7lx^G|2?y7wPc0B5djf)AxgNgvEj7?Z?cI(lpYxPM zJMuBGGbd?QEMpLJKt{=oQ`_^Y^3#82JuP) z7spahlF|fY+WwH%qB<%d?7{WlyE=j5(0 zg|z45MrrjDUCWt&d7rB4hrYQ;)b@>MeXm2{Eq@r2(weNxMqILG%Lgnx9Mj@*=uL;p z`}BHVors;==}z)MoV=QtZMq$GhT?;Uc62ND(4$zXNdbYa{oCfdf8EPR>?0U!`IECz zQ&ETk`e%=dTUoK5I-dE0+1AA)AGY-;^9|QdP0tNfF7k z!N9v=!i>%+n+K=fD<~^~mdxj(T$uM_-K8MTZAbTmVqbPGcIo4wr8^xqUjA#JV*TZ; zo5-P5jXg=i75p`GBa))ocjV!7c*3(6?vJ0&sq5GR%x&Y?r7+dscHAbkjvK4Rk9%~j zW|fC+O{g6`8fU9)h4^>s_R z`>n>Ic>z}6?pIm{n`xF#U1X6R_*AK#eV(xLG?AvaS-QC5y%Y;EZ{Pj6s?thu-s@W=g&xCkorH$2e&hjU12s_io&HN?)ZXQtEcD1 z0{Xp654aQIiam^|@oAhvJ`17qI%(UL8ar)9oA92Wc$wAQ_Fe4sJ9&%X`m*WWWInNh zV9j@mKURJk7BUdV#Cd(Ix7{l4SAPBHaEtC!ro0USCb$ zewS-d9$51}s`pz8_Ton|!oY9oW|A1uHK_KmhY$6oy}Rx)KbGZjWV!lO;h$cUv8%g( zwdB92Jcb)hB0dC!iMSXjIYYOojV45rP(?=Z%e;rps-WRobrMa&62Eihnh~nfdrn>n zj!Kb?a{F#Y`8xI;My|y@b$w)>}AQQ zUat~eLYsKcGO%be*6|{!+UcGVKB!uH?iyB7M~8TP|clFlt1x^>t= zXOy@}eSHJvfxmOSwrufn`|Ki_Bb*ftcO&VsgYo}9^3@h?J*6Sodq*a314b%;&Hcit zu}3L%1L#IA&8I8`Dy18Q_oYq37KXlEZ?B`oPw-dAr? z?V}trG5w5`zIY9F_J}=U%A3f~!$>HG;-uA}s1FOIkzfvNE2Zz{df+^kU)AMvb{u}Z z9?UnBgl(+0o|)n6$2ufp+0{LYK^)l9+pvMi%AS!zxncWv z5pE|z#1l$cch!hv2q;$@r8QiqTE1z>7^SbgZpq(2vhY1Be)9})U*ANA8Y*6 z!HJtw&i|=@s9NbsOU0oP9@g1+~avv<;)h_|Gm! zCmA1!lq}0OSlS*WF<*EzR;oK8cKaCYy{C-jO{5)|+rsf&phF48L@o^>h{k!;gql$+ z!fI*Lg>AgdwT@LsYkdMI88Hy{EbX2*6V^1^onQS8qlJgB`cTz|F^WJYjr8VW-=wcag&K#_IKIWW+9!?h_k$pj=9%HCLdbsonOWCoC;$tRcVR&r=uokPP+nigaBdzbR zbz9zMsBOsCN4#WMA~ouw8S%ix z+V*3Q5y8IB+e1?0tX{ir<+&n(N*84++C4ILOLU;~#Of$F-~N}mhvOI$HQpq_HZbJh zGp|XTu(u=Ztx48R7HBJ3XQKHW8;N|X%&)yM8TM|5N1Mjqa^GrKef-v{QkfiF;8xZ8 z9XwiO#$l3s&5n$*8ZujO?hp~ms+RPvII*%*4Mpmj)=9#Dz$62s+Pecj3ZR6^-IhUO zSW7(EOJYX-pvdYwMGQF=b@|*KZy0TYckUwzPjr4A4}{bvU*J{OiHR&< zHvb~?bR3VxH%E#GQ`hqIUPB2`2EQpBF^GkBplA+fs|uhl&~%RkNnszP&>A_KWr^GF z-j-{6FgTVMc6fEqp-IR^-MuGW9Y8&=SE&nHgcq4*rlhjku^VMm;h;QbsJzRu<u@-|aC5q~X8&;%te3A&AI5gYvJ2YO3zxylICSw1QKs{o25 zi2I;^(+vvm=*WM)Yn>51n!=EclKjClpeag_jUom7un{%d`EJ!3kLnmL#P&DijsgXZ zP)fS+gi6WhGii4zmbzQPaUsBgK5|7T92V*%I|ej>Pl#BSBF0tk)KlljgzenDvy&)` z<)1LweM^hVcpT-{D2okarAgSA%34ZU`BU)S>$R65YX92UM)|HQYAp%-tqm-%Z_*_= zO3dGrC^1}>v9fC_Q^m?B!)9U-S5c!A7GFBTaJI|TTzslThX-^65jSx>OVorr*GP&% zW&P>$!Bc`&yIkL%U`^?a2qGu`+ZAvl0m^aGreFH%WMb5AWhr&hW{n^2QXBRAIkzmC zqSl|00!kIIR|`m`LgxrpjWTc6paj7|Tnk061-ca|ZXI9E+tM8-p2nz4;MmYWiy}c% z7u8!pvJ8&bB}9C@j33ciT;JCdb>{yvSnt^X5oV1Fun=OS>)UU6; zmfL&2&uBhl!}|d%!&}?F&PdqLP_ElOG%A0#^)%)n*bSjEHtCzTn1w z`%62pbc);hMML1HjMQBU*qZtOgo;?Q3ttndgOOs&lOKkteK=AD#Df%O^ z(=D%18QQ)Y+v2j{v_ej2%<2+<%6^8468?3N0w%7D8hx3uI@)zD-}SV2KEFXnXpD+; zJ2>b3NRtC~<#9y0yDzb7gOTvJqdVXW9K{+O+1tyEQl)!^sxDRKd&KTGE=l-#Pi!E4 zFl5HLhwUpPDo@hgwf0D-oUz|VEY)yLLmez1M>?ewCdQWDEg7qK*q%qoAq@;c)8AGa&c=P!SS5?+KJcJ{`)Va$jqo|?3!J9Js5d1aY` zdgXx+TO-O>w46N_ucBgpkStp+VQ>CDmtf;dxJF>w1*CNK2yFi)_JN-i>kc!c zZ1TJ3#~D!)ra0&ZK*%!1eQt|ztIg}Tv}$+#A#P+R;CwhIYwNkSP)U(zR6)0*ca<*T z-c03gQJkU2V+xy&Ls1HjapGp-&bEpW)QkWreOv(b=P|exs7f^cLs>pp#~pqZLMhQz zpmIx_?4M;#Ul^0%A}?#u8qGw8%%oqsWb|Skw|Y@mbYR`r>Zmh>|CMfe;J`(2DW0)5 zWZej*K||UmGGZ^>r!guJ#I=lbqM|NFxEZVd4@XxX59Rm$vn3T~tl7ud27{6%WFLeq zW8cXhB1>eQkQ&2~GNY2E@fl09lO;>aAVOo`)kJn0>tOJk@9*Dv-RI1G&hx(aoO{lB zpIhC!!i;@iDbsICoXPra60#cz?0>`e5^v7i9&*F8Y=IRTDa`LU9)};^Tq<+bm8r-b zqoXnWyTTgTYp!}3S(Ruc`y|*`b4AhjCm2;n1hHf1T|2{?6&wUzZLqR9)tnlW5bGc$ z;a{^wXKkI>LgTfXY#QLgij->^5$}h|VfEAx#!#`DWFLKmr`mk3Lrbk!sVhxG1Rzv$ zu9IVe3A#U>h))r7usV5ye_z-4QZlAo+`TrDhWY=OS*!d%nTuhJX-5z=lv6A1B&yWv z_&%#=XaFL>PRgMIunGIh#emhrtDd^asitdkMxOT_q-8xv48Twqilu+dO}0cL!;`}2dvoi3=guW zU$e+bDbyMk62EjuUbk(Pwcvyc+N@5FPXqS9&+t%FxhrOhX17r!)Y-%Yp5L4!5;1)!`CvivZ z&YW2lIpQ*%_IRo0I*}-&M;C}K@12klZ;ofT%j%C9@%8MO@wOl)Twfcq)HMp+GRX_u zpQ$~)DqwIH#O2t?4996A*=o5zNYDIr4Z4hzYqPX#A&zTq>jW=zGV~9A^0+@3TxrDS!8bz>9hpvA3be0&d|>I=o;u&& zpH)}OQ8W858;U;~7$e?3p?WRwK-rR&2eqButXCCw{=J7{##0(D^p|aQ?bKeWxx2x{ zqgHUu!A|`N{_^MdgY|T#1%Ew4qb~Hv8&%)ZD1)-YLJs6jLe98TmwqySul>m6#plyy zH9!YdNmi^cm}T+d*7x)5!>czo459YwoN1;O)q?eYyLSbu9jbF7HOdZ7Dh#H} zDjsT6Owb1{<Z>(I!zMZqd=HVJ|zr?HV7rWm! zue}}A=b*w@Lk(cUA{dlvuOf8uf&E6GW|olncN=hOrh`m{k|Xo67ZPGedhNg-xk z(Q0*|UPu>+5q5H@T$YiC%U znnO$jb3fi(#x#WJ{8$0sm~KTn$t)T>EIXoi9Y9L7s5y)uPV~N9md0#NIZJESWz(jp zPm`KK8}=Y|DU7#T$)6sZ?(4x4#DWbN_)`0s&Vfyp!E`s$V`>KObDtKW6fWF5X!?() z0LXX6f=q0NAyjnW3&*&HSpRa5&GCH%eh>MQCXIFSP(%Z{t*s6)4e+bZzJC?lW7ery z$>eZZg{hz4xjHELGFIk6C&!4cPfdbeo6t+BpDAs4MmALHRzF0HXQ-Iwdz5D_ZHdXW z4Y*3rwLPVr70I2Pd1;VOdsMn=+c_P6@Fy+>aaQj=V8gnjJ2_lLpr&u15Wdnucc;st zSdzqNWy@8$vL@P8AMS@oJ^G)qCN#4+%pUuDtDVs-{~|p64~aI_{i#Ed8PvA+CrNB| zhR4xQ0Mr=W>A7$fY9Wy5qo-L-QM%ZlIqvah*4*bipluFYB=O-jP3ZOSin(<`ik|mJ zuF;HUpttd39lXMUrHMU6vi;m4Gya^YOZ%ev4z21>mh2d+VaoKT4aFX6AL262?kw_~|7595$U!ft)2fNbp-=fj{~C{n^th8xDo5Mu zlIb`qW@JbZ>exQ{?|w$PGoF5r#r}xG50qF z^EKioUat!jFn?AY~Hi}-U z9Bn9SIsVSzvlugQl>gK~g_=m+skE{Deeme!&d|HvWHJTLUzmpsWC&bX3{(R z>RiqK%B`eV0b=d0nqAs`pS;D^z-P;A$n&a7er>%yo#jW*g!$p@G2fQa2GnR)M=FSC z;orLE_JjeoC&P)f{9`n#ReDfj@snSN1YyCJspDul;Q{KrN|Df5mDH)-%md*cVszE8T2KyGNpIn<5=&y!Q9(COv`3jrPdb%!>z88CMmk*b%seH-twRxv+k}yH zynF{mFu`DUJ7g0Q7Gs7Aa^*{&?YKy4tV*HSPswVHT_lG)iM1QZDW6>=>VKSNkLXpH zKYxTJud8~HZ=$yjqg0URWkua5ZRg4|rw;l0`9ntJOdB0)6f0r7q(X=M*A<{az}F|`M7++D7c%HGmxWscJdZ^-kjV`8MH6k zlWEp%dyzvDk;{C92=GE_>t3Z8d~!HsD8IM1?!XB>AEWQD=lA$-%Llbf_eo%={!vz0 z`!sF7<@=_k=9-yydx7_nQ(8@ZZ%+JNha&?tY9`R-aK`2R@W7&3jTC!Lklb^4=m^$9 z-d+rw6l9ZLj-u>4P#IK^+ipVS1zQ-k0NJ6l=doS5XIT*Q(_%&T&w@e^iCu}GyYYkA zSz9E(QOY+TnKtKOZL1%I<)3|?{1G4I=Pk-C$X75%i{x>nONTmG#Jh^}A^=HkPvVbm zD4o%%ow0Up3lZ5rY<8^cy}9dCaB-wubf^3ie~E61spnjd>R0kCms-PB@Ml_E`MW&q%w|Ye0+4C8kxo~H7X!xgZ zM8e-m^;QtLZ=&LAmB6t!+{-MFq-)I_VfoH{SwiLD9XXZi_^F_fjNb3KO4lp+%DOj{ zvk-;-@Di%6`3=;&`{>?cC$U(U!eQqn&3zrIK~YoD+QtS-TZU)&%zszG3gu6G2gi}` z1|vh?2R)m}u@)2IepH}hzm`$ha?GkZwqWo6%HV7W5|+R-^#R@8JHJd=7W7_JZHDX= zy?xzBUvBB0hn#I!Dy}X6gzRVqlRn$%QkQLk2sfWD1t&W+;_hCxvv6l6 zA4jT%p|xY84AQ;2p_?)?2w%M!cJpXFo75qbM$yF(4p#%Lscd+_xoLb?=Eo_aVuY1c0^1kOjk(lC4taqK8_>Np>`9cD$9 zBHZ|uEDNX3Hb4@3ULpmZg|m!o^%kpWD~!pxRfbkw>kfRB+b1`W{f@L-FgLbfZS&lq zGDrL5iGnRlR{vCBx?tQhlk*p`tL~Dt0YOe_rFXK@CM_4WBmmL&YOs7OV3wJ~V$)@$ zOI0@iQDW(gD_a;mWM?dlI_U6O1SQTDNp1k&WC4vZ? zk(^mOrPXwmCaqQUEPoiePz)hOeWRzvgMWT;TNPboCuq+bpUykp;5ko$Q=NPO9d4Xx zvb|u@FsHK&ewXID8;kxp2*pdcHz*TC9Is_%VssW?3?p4$&uvTN0aLnFhG&qq?%ozt zg!Q7Vkk8^)ii39@{gmHHF&^0L{`>TR9*skaj5aO{mV6xq|(W(z#>E&Jz+TfE`Yjn5Tyt$5fAYN>((lJb05gJX2KF zWmSwmK2vnB3%*vvxZbH)mEY~SKB6V#+L2`ya|<)xyhyMY#-*W_f3|jo-4Y2d9Zu2N zE4_s5u1%doq2HC#gQjg5VVEiYL!{=15ZO`=%%ny9--@!m=wDZWd|DwNsrjY1(^sU# z6yuie`2zvykL4+Ma>_bh8{++DWbiYzgi6uv$zE1DeHg=KAn|Q0@_ghnnVrjb*BIV~ zu)jnuSvH~;ehr|$B>A99EB7A$yGb~Ipzvow{tGGRQ*V2gfZa8+(HQaZ9~Tq+aOUFH z*F=-`$iGI^d!Ay;b}vXq%equ|ED})-AVvAA_MRjmwZ%k8%)C~AayS%p?>CzZ9mB}y z_1R=?TG@@SA^AD49+lXj+}+WdV{+(UnjK6_dzyubojt56i2fC{*JHcXO$fQ)|q&g41|xx&|x#+SD3$%TOFb{&@DZ=7McWKLm&lbfaW zm1TT?pC5Yu>i}5X_$zT;n|e>S|DNm*_Kcv*r@e;A5MjVT13kR*C8)3ZLKYftvS+&o=F;-eNRCfJ`znHvBsG(6!yFIwreFrP48KrJswDxNX7Th6QSvzT20 zCG@ZH8|k@lVB}>QZ%CBZ%THb<=c3x4KF~sK`=BT}yP&gSD$;$PgPggnA15V5V(+c$ zJ{OPqS5>lCy}?T8n07eLH#g_e{su{?qas7x&Ah1SR2y}xN-hanI^#c<;HSPZiR=Y_!S9#05-V&?Zyr)ll6aKbIqO8&4DY@K5z5F>- z!B1O`bclr?3{hjBOO@i;65G%9@Q>OFI`GCzaGN20E@8>S>#~ff`q{$Q7+$XEsdv?y z?2xx_lW_5F7*gd*$pblj!BiE}vm@^~_9}(%F{cX{l?x+NQSoyWf0^YK=7F9TN3w!Ye1^UPvFG$Awb9e$w=gq1?bjRHNhoZ~7RF4b&! zpFDREf*981F3Adr%7!K5YFQv_wM^NX?*E|@rn7C2LyHpj%K7hHF8?@abJ@KJ=o!F< zn#Cwr^8abHUNwA_J#3NBNf^-PS*ySA^a66Wg2WNDuPC4OxgfW(0%Z3kUFyS|T?$zY zHOQDvIiUoKQR)i9jLNRY}XW z9LtN22=7}`=V(g#T_B>6kJO=JL}gO%Qh0O!eRrdZ^`C7{&Bd3`8Intx<89*i-_b7U zJw8b%t^WXMTNt3pnvL+))g{dEP(GYG({WwfBK4mZuY*?3#4YVxA^2f^ z85z97Z3%3V_6?c&G*=&6YtOQ7LxVyVQj|CO{N}gP*S;u9^O>iO!;;mXo>XD;KK)kn z(PXWr%HyNR_8iIsB^j;P;Io`CnzP@}BeMx=wBjwWqdj z=}OlphlxiXZ}K^zE!x+(nfOd^X{7zT33uc{&a)X~k7?&)J)!->o-@xP+<&$Q@K9MP zhb_$5Eg3eR;;;j1 zgL0QFe2tQ`d$Jtlj!QhFl2bo>b zf1}c0+`6Hc6wYa|b@j@lr&n$N(y0$AbzU;|9J${i=n;L)DYmH3mj2?#Ml0?Erccps z@2}*L?8j2o_z9<%_`f3EqyQzogT)J`PWRDb|LNVv$cqoLflqL zYPF)kK|aI`-wLsry@T)R9qt#gq*hlU3p~xqQD=BcLAz+egf`OMGSv&&@obK(;#~AB zMFfdBDd0!6uH3Wa#y{7iiHtSl>p-#Os@Mw<7Eaj7hdHCyI;;dSXBPPeu=x_oZU^Yh zt)_f-|2YY9hN8?Rh$Xy7AVu;R$TL&}YT5$ue9&F_jFG5+jx%9TDPwN4X5~BAKwPl6 zi7GheC9`M$yw(x+@s?C}?j-jiB!S}BWuaU|N%fT7U%EnyI!~dboP*D%@6MjEApqyY zs$Cr?M(Azn7>I-8>ibLkh1+n~)2OkdHpC1hVOpe1+rh<)DkD(3$Lbif2Xb3jx$r+;dxlY~elH%lawvb$GcQtodCd zEF21u*||rqEyQSI+Qkc$?C0K7u4sE2yoPR_{`E9?c$1L--sqwoqvnhWp<23TPNCZ6 zh#q?q^ghDZ54cd>83}9~gW~rNwpD7B%4Hm9JL=%@j~)lyq6fSVD%@jDV25tY)vMn* zbnuTxw7hG$IH1b}lqDhEVR|G;#34HCv0pQAAZ+bqkWZU%ZZjWsVEqjBJAPTF51`n( z-D=9`^vvrYgLci1$_>J^{|wkZwpM9%s!=uGg&>l`Z{^0tR5QPca){BYDylB;|9*6i zrBL;`R|UU`cnREG)_XRjO??_J+s3op`sSF@U=ve`w`$?Sj61e9T56xxY6QOUQB(b4 zJZP;A+8GN$w%%sQlC9O)5_b-cNw5<0L$#onP#wP|4H9e|^FkhNWC(PD1+-3wr5Un5 z^AI_v#H~9t-kJDNSubd*_=!UahcbmOf z4%f!=W!XpUXHxU$6@~5R{f9QOXgBh`1wgcoF1m_>AG103xq+`|z>F&=p#3Ui5z@6Y zyePrbi>BYJEFSN(ComZ-ixfT{!oK9&)&c&2gn7o^mv&3~78d&^0u9l^+HXg-cQy?| zt`4~jy6gV-#gpHxQ)FZ`UCcxOS1vdA5L+JSyIOON8r4;PeiHSm!E0kVD6r==(?P?t z-D9(KUnsB#GvuH#J5L{}G}DZ)i;rEme=-)&ypdJ#WUS5+-($)hnR#d8PWw-Jdlr@2 zcTde8_j`2QaQNnj4U3}-k~fvB{`=#C-qzqf|y*k{k>nVLz+1kA8?Y5ky>(E7wqK^JBxy-6`&w0%IM z>zqPIj@K4TIATY3Y)X)}PYb)ZWn!ALoL7I37@*ug*Qa;nh722W_q8V&&_FeIq*Upy zmXkKL-uK8`1~ZP1en!did}X*E`TER6oiyPYI8}0p)PA)Imu)qoKIKIgpc+2ruKX^Z zRgQ1hA+Kj!Aw;ALzUZfusvc)j;L*z6s;-jl+gJ3o#4)2bhjg_4etWUhg*o9Vr@? z(7~cmxqc*|;Q5cLY+;%ozhzPVir6NG`n3%UU6Tge$DnD^MevTd3MH-0V3Bn-z`F!* z6Xj!!-coWzd=b}jf8lM@tLUz;r4gof{(=G-KOUt*s+l^6c`Ax(Gw z=;qcTRzW9gR~f?7%4BeAUwDhc$mQprh&$!Q;Br=^`e5%Vp>HAlT($_rAg3X`@_%9n zWG%Z^l;*eprgbd?7UP#`>f;pg5dU~|m#g0$Kp1$A3^@@b)8IzE5GiI8pGq;#1fByT zU$+^s_wPV7z2D&|tov&nx&Pi0LhdVZ;M~Dy$1XJgJwJt3mj1>nMiMh_){;pf7C;gf z!)bmU9ZB_$5jC9`DMH~Vm$&Iq=Q083*CSKoVeGy;sjcK@=TE*hx=XGtFdOazUTkAR>K+qIroYS|ruDXB6!Mu>^nsb!^HrhYBEP-nRd9 z$gK%GvkmTcmx3e|eO+rAii&SHBu{7>Q77N=|5?7myS?2{siP4F*5&^u26)a@{d+@d zjLwYyvLy{ksL2BS2!6`HS~$6Tfy-z_m&l>Ow>4ZPrAtAff75^ zl$l_{iQd)8IPw&JQI*6?!%Mr@do6`*;I0nQ0!CaB%I6Kx>fHVM-0!j5zi7J3u%K_P z`&vy_`HB4pnoSA+C6|u|KAc*xZ8k~EpZRXP@$Uvn@wPVPe7=*p8`Ir%b?Ay1ZK&Au z?w)el-LB~NR=*iqMg7RkH8u9G{Cht+NlsDBTnOPr()AfFH5wt|52qz%o`K-<>Gj{3 z5-YB&MF}!z)^6>KMKJP|l~s1f0Ax!5EjUq9cnaN*=6vK%O-LnW{@twoC@q55r%JZ#uQ-H-8r(LFPe{pKI%22}x;G=Ip)xQZ z&4>j?eNkM!;V@s9yYyWjvy{9d_a}4Mx$B;1vy0AY6l?xX?65&?$Jp@V4F@Oc*w>e` z)*=FW_`p*u6W)^H7JAj{KBZ_g=)MR?|6(o@kvhN)geEkXAQ2m)q^J%wr80ArB4WTU zvB(p+DvCl#bfk-XZ*-7DdAs|()zYJ45%pcFac61{ac7Qfy&SlR>faA1#n*8kPYe8m z5xULi8ju5o<3PiFpk7H56S(fLm3~PrL0v7Ir!L{|L(a{L(^Ku~K%vO#(r+J64C?*v zS8^4Iq9&q?KQ%hLg#IqyIo|&td97!<&SYrbEq>f>gS9?KKQLA-xs)Oq4T(6sz`adz zdSIb2?JRFUU=vC>Z2hRYygW4%*fl*;b1;jzAG93-;;wpODn0MQb-o9C5c1*D!x5Wi z+<#3tk&x}Rk&R7dpG|aA#zDr}=%m{t;=JSiI$v$>l5KM8K`=_z?aa>2MWjV9YWlk0 zYWCssSy8SvA~Sf9>HPP_lYO1GP99Xpa?z~LB+4W|dXZJ-dfuFeyxPO$)!KxWLlM3C zAxg)b2R3Z_fq)KvL|YnvxK(uN`?GHvH-Vj}v{svs&47ch-{L}7FOp!tZj)$feeHO= zv%!0NQEOg5d<0{)+|#{6f7B+te#{yQJ#*-}9t4DWHv8Jp>`=(_u= zPM4c_0LTkEQCi3h_I*?-~VLT9^GG&=YR5z7r%$)l8N7aseU0pRrd0tjazav z3B}nwd7;Ti=|W^3-MLre+tm9eAkgQHiA{BG<+O$@#oC$7bv; zj!X@!X|8|HuSyMBIu%dweC_+6)3XN6(=@{|qnl-WvdyL|ZO@$s!Ug`=MI{~!A&A$5 zmyxgY=B2Cje25=DmmQyAwyNz$(A}H=rTOoZ{Lp_sz4srxdpX2h@^@{JL2Ue+r$d^> z8Hes)I4lJ-bC|mp!vC}hp6?y56or2%*lq6tx2if3BVBZ!#+~Ivuw_Z;>t^TdnemE?$X&kNQ5~;7_y)p;P{ud<9yg?qiAV8Mj{nlp!6}V*=6d}g3H%%J^4G*TYBKr>s8Z#mQFqzIgfjp z>$`t6+GgjEjl1r8+rGorG`Yn+u9n zir=*u9_JgBFE0cJjoH;tyJ%H+ZQ~AFg_C#p`1CWQx>E3NGQ5M+2HLYZ`|9=cvX#*5 z|6C3*g^etBNp*fd@%!g%JLioYPc4c_#9C8MICbIbzGR5xbgZQPujd-~yM|P6-;ymr zKoI$c)UH7B^P~D}1GIz9JDWk~mBUM}+~*H&6~-3MazDHIT&mB^eEChuzn`Z^>XKJJ zKYtD7sQR5zoA6X)O*vN5;h|w$Q5@{+r?6T`qjlWb?#teqzAcM%=Uv=- z)5tHC)-vz8ARa|mqk*jKVujL&dcKK&h*_~R7fl)pRsQNmK7?YU{$4na-IG1*Nt(vzD?X1+q+$CC5}u8zJn4T%}nSFF$NOc2L@=Fh3ZcS;*f-n@jExX zQI8AsDxa%zAaVcngWhf^00}-d({1YSKb^W#7b5T2pM}^;?mmu}wys9U0j98;(U5)nQt-W9Dc_QfUdpq!Yr+32NbiG2BS!3`?HXzBJ+gj?J1N4QvxXx<0K9s1|g&l~gPYYRAjvnlP2!V|Y4 zaf$QU^f6F?be?K9M-tClPyLUOi*Bp-(vMm||+{U*{BhCqC zTyh2c6~_iV1GoJSINLr(ypn6w?stmpeG`8y6n{;w;Bhf|tQsh{kb|EfSmz$6O)^Aq4BauyB$nUn zKe+IG@B=V1t)pb!VrhF-L}9R_qwkVCW%H-1-vM0%830}!T|}M%I)Uu{_G#x4_o|e}{Qum2J1Yc&!a= z;J`Q}#VG; z?vvodYoQ&zoTLESe@L>{6`n|wPkG6O!qx0VVPV~R)*j%%*<1dKTBOQW!_HyBybduU zpuU8vtLdJxQCBFWk^Fk^j#vGRuuaUo5I(|QrXuha%KrTG<9%(UwM+T#kst05$8<;#~uvDTKq9n3T4FPi}E(@`Fl3k2|%xOnQ?_>IH=^&n~o+_ zU)5sdSt#;w%x1(6$9@TWYt~Uy(pgJ-(#IhcGXoKcRELM`MPKY3re5$eR`fK!w|v2U z%ilYcPkxXtAA(n?6vcWOQ^dbZ-*8e9lESWS__9ntr{uAL6ON<1w8O8F|4a^$5_HiW z@V#m~E#|0V;XTnRAF5Uv^o*6KAR&`3_Re{Xvsc{2J)7gEIO`=VT2B#dI_LY$ldkB( zTdA&L^kVlIXS{ygVKC@tD12RWh0FJ|Zr0;VTvyCLaFrWqBNp|7EKXPA-HlOF%hw31 zhWBm(O+1+CtAhVOF|5IgAJz<(#b;JYqDEb>v8>*Q-5@E!p9&hq71(q&E@V=)G=SPs z93&|r7&XNq6bED9UR({8UDMUx#iwg4r4ST?U&f0E`C05$g#@H8cwNHCOWVKWET_dp z&4X&rgbFF_1hl0VkQjFeb}ElYMzRZRh>^D4B@WfTt!__LAw-J?j6I3`0&Tou=)4OK z0Vr$(;q96^zW@8l`o0P3;2|^4mV+3U>c2Tzh&I-(|9v0no8&)4SU?zOyo=sDT({87 zAwCQ7BgXI<^m`2bWDWLD{}fYAne7(C*W;1cKxW_lHYw@pN=~j#nVB2;v&pg1ZZa2l zyHBtExX*I!$L5sBSt~n~H#2F;AJrFTyr<%Dpc2hgvvD^E`X^HnOO~AS+wY-4@W&(c zh|@2SOhw20>_MbBHi~7^Mqp61&LIP3{Yt*1#%wz4D(-u%Zv8_Mq)HG2JWpsNLHjDs z^gd`hh7o2P+7ZGTzqS~$42xg;R$xr?qYUd`1Nl_On5WdfR0Z;<&N8D;;2f^*=CJaN zWdGLJTUz3r@VQM8$}={(`Lpg~g8&RYB}whz1Wo5&#^HlPBh*=NE*iEtOQPS~jXIwF zvctzRsfM9AZ!z51Mkn>JtsIQTcs5m)In~eKm68|3Q7RmOm79{dDraQrZ@Vs$yr-0g z1JD`YC*^?Nd|l52@YxUj0BSUjdE&DPdqWxX>1!#)_7fGJ^GYHL@aO_L#Ff|to@6f^ zZVZ;ezrtEk&7806T|^f3ykUm4PqCx}GGXa2roUek%{xr;|K$s4o4*YH%m8}ZQ@j8S zBF2TmB>1I|e~1>`-Gvt%XJkqck&K3WJ^oMc`Ym&O&rzO-;av%b>A})?UIPI9r|;bk zGe&s)jiJ41b}ZZHFE2CaZrM(uM}jyV%pcJ8FKT+MCR~8BrOKqeTB&I9K5BXDu%`JE zDjjF+4Rasv#f85g-u78twQ)L(6EHL<=y#E-GaUv+2)$*WIbrUcy50@U zFw7s_`tX~`v+A3yCEkFk4Q8U`tV;u|z*<2z-p%nYjE7w!Ye36z(IWy!EA0Xor17Wb z@0AR@FAj`F)jZysyo|eBBPCs62+N&Hj!P5*wR|-sYN(*@sw6?)p>Z=o#}x#X#puaT za+fSVM(34TSRYvhyTf21Tc)^y)O7#e_0Zk9?l z-|~gsGlXed>x}+uHN=&++qCo<$>F-u{}}>%)n}rp25034;S30bn)Gx=H~WNcyNme?knvHiIEc%6TPon~~VU1;EUg za1fAM+b-6}K}PXwr`vaMq+C9tvKa0^Z$SL5qO8)85E)J9d4IoUPI%E{fSI`j4(}=J z@b?0e`sqojD?M^HD#_pED)O$U?x->!?1qMpQaC#@q4)!=4pzWfDI0J1PC@JZku!(u za^?%|bR^H_MdHy2e`-)He6^-0dzgF=X5%e}e0P8fe>n%+3lu>vYW3*YgbE;?wl2Vj z9)V)2g<;B($^PS!fM)UJm2NjoukBv5JBQDp818T5X!yiqLz_M&4Puix;DHuCtM@|* z<@UA#KH}Hy&@KM7?bvY>QCzSwEW+CHqvn)W&lk*hLmTPHkuk^qzY;3Au)1WU3C4%Y zp5-&m{tpR%fMbSxf>Np%a`aQnznFc0l(7(Tklz0uspj75njT1TQHW*K=Ba zpGM;lntKA=S9sIZC&4jMYy()zWF{^RfMPzg!qjlON(dojaLqT?+y@{<3wro3VGP8A zowy9d4?S&$?H#v>fB2jNWL4#d<0BJG7g*3@YaUUdmEglW`!vLD#sKEHhujg79PpoR zfHo8Z{Hr7&@7|FAVUdnfj2!ail&(>J1;}K934G{{S!rIWdoBK!7d(hKdj(^uNr#Lg zAzsW#e!+=5^)0y+>X{n8UV=sq?0(`O^73(kKQoZkn2z|}0WgrpOzf5enK-lJ4u~M9 zS_asVYVyj8IiO*-f!ML8wm$36Vs z3_?y48!U&E+~nw}l1xOJNG1CZ z#sC`748&h%pdBX`oFE(&<~_3C@)&Rw%na|A$M_eo)SZgrhc1tnB>dvu510I&zLE1K8e87Fh5N&)ZIAlRFlI2(vp;;)rhKT>jRB84mH~fS+@NoLW+l zFI73hl3BW@#2f^AdBQCg~bmd6o=TKnOH zCb;c&KB9ye&NUPilk*{~G|D+NL+rQjnM>8<_hV8(bz&1+5FI>)8)V|n1b*cYSmjNE_})OyS1`lZl|eF`S8y1z417vu zbh*qMKU6f53fcWn9Q7wwD%SNtpn59?fSxNNUu+#WiR-Z3(pR^8kCP{B+bMavLFh|6_c_@RaoQ$7_u@o(S>khtCes-3UMg0$i(2?cR? zZqOp_ zR0OW_JSp8*2lI0nz(Q1>l%9A3xZN(})HlfE_vDfC0}Svo2*AvN4JRL{6A_SSvIsd& zpDEir3grNfsejB58S;V=e$;ZZy0GGGHIz8sC~+@T-L)~Xpea~tSJx&|4medMi5pqh z^{%@F6Uli3IS&J1*y)I$ECD_rJx_2xpbtDuxXA_?_#>hps~SrFt~jpsxj3;WCB5_w z5ME~s+L^D+TG90)pC&!VM_9;IXmDM`Ed(M<_hi7RG&*>G3aF*}8mtPNv=Rb9>MbX& z9Gd`a-7kaZ0suz_|+8RbN!fZIXG@S@VdRP>_t`e9LmMW{`%|nNilGRmuh&I`ywXV=P z;8RRQW^Cuc_IYpOE{NemvOpgi&BTb8HEI@KJjWf;taA(m`O=<~h`s~nujhgF{7r^< ziy~E^ba0akNWmoC`s*Ap3*O|FpO*p3X;!IhX@p5& zY35nkmP3xs2PTq^{Hwr3Ts7lQU7NJJFQtZ3NvBa!JpgKzAY7dHv~p!sQ1=|&tt znYMI;!*wEhL}0YL5R|;=kjL^2#4&CexHi%6oWZ1^Llap_b~9FzMFP18^YZ*77?C8B zGRIPt!eEw)G=BLpa>$iAsWe6$_$UK}3*6W!CAt7k^OnkV8Jv=|vK|L`Gel1uRs+SeT@MZT&GhXAc~xSp0u(7 z1~j;T;K<{Q&(E<94fVY`I>Z-gj@`lGJtEVRQ#(T{U=9e-?OSF{W z9GX}CU?u6u;jg-TnucOoxvZEodG^~?SH*DHI@-s+0jyw?!u8SY?}P{}Ll?xa%nNHD z(eQxP4S-ZLs5AqCg#mZ%S z*Dxp@*$Ef_z{rL!1(Mm&&vF%rbzAo6mQ<=v`DT%U?6+H5;#aiyHGIKiZR-)n!^|=e)1T^)-%%Qv-Ap3MwtAT7l|ks!a#gM9ix^v3*v0`}j}8dG9V)?q9L3 z{D51&zn+~x)XbT4#P;1blIKr=f7h6|a8ofXaIwYSjOup9ahg_B%tm>#?sqE`^Ulac zfG1h@Fz8|4F2h8c&coAu`pOsgf3MZa&%%JMl@y~x;$3e+4V?6 zH_Vl!@z7qZvi1G1fDS`3dp%p3IWu9H4b9bLx_R9_FVDgy+&*mw#Nd<$ z?RP&Cw!cl4W}m6PKz83{hR0n4=CO(5gsh^%};WHTE?tvo?=eiDd^ zv*~*5ED&`J0@3lgDp4oV=jRBe&v!0FRAZAQY8A|g3g*d97PrK(l%6Dw{jAQt_Kp)_c=r~doGBcP61K;G!Py00nyE|AR0OW zL_6lNeUT4{9{PdEHAp3TB>L=p{c}LnI8Y@z zAo|=Gt@P>Mmx!V_NTSLU7*UK7(WO}+$`_%LXi^z*zP(B*{k#tm_3{MKLVqIKIg5yz z1cT`RG502LHD-VOc%dokD&^XjkQ5@aWQn4w!=Q-zj?!{c+-|s)adA+Ww+j-7;-k)>M`}01Z^Lfs* zY793~zp>mzlgD!tEgZ^CRB<>r(U2+JMEjGtiIz>}CekL$CR$GK(~u_JXA8Vf?ZmH` ziB60XOmtLbqOFs;iHr-aG*QP2^m%_4&N|!`P2@Y0n<#ENnka2DnyA!FZlVdJxrtgN zauf9umbW8@a1*^5#!b{_GB?rkaoj{>r*IQJn=YGZoOGXSrTZ+8_ois9>TT zm5Js|;wCzpZl#H8m#5F$vh~`UE@&dn2yUX#Y4p87lh8!Zlevj{jN&FLpTJGjSXk1w zkL4!1I+UBJ+9YnGabvlOx=iLKIyX%=QCE7OiSwlUeAO9E)Nloy1MFb*gNldh|Z8Gj)BX6Pn2NOJ*XSxazN`GEwUZ+(c97TWO;E zWw2>`w)W|TP1|J;<0dLR1x@5J0Zp_`h$EF4$xU=+6gSb+1a2a`!Q4a(W4VcLj^`$- zm%vS=naE8vXNqj1;$`W5u8{8Y`*1YTlU2+_A;Sa{byJzheLOc&WSW&GI$Ro?PGJ+R zF*a?tekeE5ok?gS+wo|kxkBvd@o;XU@8h|N&W_lY7|Bia&1i0-g9EsU+7IR?S~`xKD0dV$QFanHQR_*viH_6z zTsT*{&$td~qRlJh=SN}%6RlF2==xZ0qEZGcO*C1HP4E6vYWisynkXukn`rq2G||4X zXrkaL+(es(aTCRj;3k?qikoOz}%OrXlgBV?F&XD*Z=?#6 ztXdq0zd%r}{|LQ&H9>~|C%uQn#nL^@Zin!XE@klj#5hbv0Y02k^>Q?aUq96v_=Qc{ zWhJobpJz%vmp9KZ)$u<4d5Qq%RZX}cPIIkaJM2u(i*p`}H0XgPy9v~5E; zw9q6OS{-V-j%=-QQHRi+WoQ*eX6~ti7C4GSn>5o(Xm_1yJb^{edSKIb`Y1j(lSb3? zG-43i_kH=`EIydSyco-2ju6-B)d>{$Io(A5Ih?FBBld$<>2La>!pLTM4-+g`CNla56S+H#(9ZiRPI=&$Xa~ zHc$|>4F+;Q`)V+!sglCt+N(D=%dS4$EYU-_S(?Xlv-l2@%@RQGtl-f z!~A^^hdEj3{eH1B%s!+}WKRz5=3tsnG900qdU0sI`*CQM1!((5%h1-)`uj0x z!C1d`dOCw<5*>}BlM>qZ!#K3ZQ>}#7tr(5VvB7zw6+$}@!J!qR(C)?|G!FsVv}g`3 zcmRiXXOs->D6Rh%gEj=~*KSxKpYJ_Dfaa-!M&lq9nwz!ICKSb{2P}}9ejJ3*?)Ko& zfM*Fb2MR5;Cx`ZRUk+_Ze-6!#L;H)?-&LruN`4{REg-`*4^m zqB+dQBW0Kss7VL0IiA%LVZNOwzn7=K0CR#0W}R3LbLb>1VP5zQo8H1Up+c}}yQbav zK%LT`?k5gGm}|OoXa$iRT2@~UEt0QLf~XnGv2B!lEf88&8CqUf0a}J?pmvGj&=yRz z652~UY`QJenM1K@yQ>1dtm}u+QeqL>(QX`C6#?44C=PA_Z1W7;D$I3 zb8fT@b1H4ieX7(}HEc^8Jy$*-K=gfQRfBWkAP)1)cq`p{gCf{;2qW-K%@O9R&K%|m z>L7mZjW7!fF#8J5qM9%V4-Vrn*Y%ZQX3(~B7^61FwzTj(3x#>Lvj8(!1#`B-yh{kot6=`tpTjI;EzGI5*mMR{HP4&S z_)kY6{zGAED9jF>ILs|QILr~fILu-K%*T;3%)7L$hs<%LHlgvKS#lg$TuQf6!Th@) zhgol|l`u1Gu<0rjrKX)IOxuo}T|*)frsn{J857Q-9q!JdrS{~|s`1r?1BI5uM$1-z zgqAZ?K1aHn0Ijb^$*$!DXx$U7WY_&B2WWaQqn&!#w4G-M4sB>}gx0=4LYvo-Lo;^c z(6&Z!Xu22~nh!Nych+kc8Y8sgGPK$XcdvrhGMYo1m|!KeKi(lUH|99HW7Br+!+2z% zC3PI}-4WW34)nHR39RYH2kHf3pbi&eKAn1T$UgmK$W5t9bB9aq-fD!9%cn5NO~h5k zVgWKl2K0S7&znqk<^I<^g+l6!su;7WMHTS0U`sl zr0*dzP^c?6%dzgA%`$4VHD)Pd(q1dTrbn?J(PPtgzjorU`l?Qyjwh868};Hj?H{H&GvgwTU{>QVCJg)(o?!iM{IF!R5(}SLm+85yuZ^z;9=*{8p59jcwcj53W3IQ40?i~J2?)d(q z_pmQfx`+Ao>H2r7?B7MTR!s%JpcjYlKhjD*`SLY3eTVsXxB7JbtK)p4?@G`0YK!nk zgz`yovLl}q^E-31%oWU1sUJ5>Az?{!OOQRwh4u2_jlKk>gb1 zf?0x#EBWMJPi~efBdjrtok=_U6*m1kPHMWq7tQjt4VOI?snaYKj%Mkh<1lx2;4sH^ z;xOF=+RX1F!+c2Fy2*M!9oy2LnJllDLj{;I#g($>T?B{Obhwp+e{DWCy*yrOT1#Qh zXvbkT>V+`tc0`!*Z8^--VH{?9IEPtVSUbCLn06HA>S0n_o9iM>$g`v{AGQ%-rl??+ z7GQRXvl3?JOFF*>Nln+mrtPYQaGlVxCw)&nU3oq2!3Xd?J%_wmkVf}fbI7CGaL5%q z%aC2Ec@1n8w!02Oem;>wKGaiyytcSfCwTPWkfVoL2|4?3Y}zJPYPwk+nuiw5ABy>t z4&a+~751(>cY@E_b0@gF1E25ZLil_?)``#e>#exC28VESxe8w3aSv{;KlI#OrNU%$ z-KF>J#JVZfhwA?cvi=v%bx<%DJQVYHH*T(aL#>?e885KuAT|e`e5n3!%_o8n^#Y!H zG*?V8hk3XihnX72VOEReFzv%-m<6=0L2QA%rZ&Qaydny7Sa$*DJ=H`gF2HOz#L7{z z<2g26h{=rV*tA`_R@~XOX^$|oBM_#sEAQpsLU}K*5hlfNfgFCUFeyrQNw1`?K#W_^LW9d)ccChdD6D z%3eP8ljaoBRWO4(bC{C{TM6^eJZyR~b0`tmwB6S&IjfKFf-pzx5a!N6K7jLka>#`R z$l0MB@&IAM+g2|_Zcfdc*k5Y*ju%3%I9ARJ6&+cmvy#Wq-5Qni-Vl0%LZ1gc9L4*9ZTyT{nHJsV)7v1z-Q04`9=(>Ys`E>9YUbBE*n9R4qD1b^L(!(Sc1;kOIr z@IARB^P^oei81-r>IlE&82LRZ9YuewV)Bp<9RBqFRx-KqAvRq!Mrt||o3`uJj8oj? zjtGBjFv9;)=w-Lo9Ol&!4s%Rb4wLD5IRA~Bx*~&_=YcSNMl+cE^a9M=DvFB=<1m-^ zvvTm;QkaF<087NC?S?kx^Lm^!bqqZ$RXcv#UZzA z!Xb|pMnbO;8FI(Juw94VQoGNqAY^!+g+hKOMrK;LC`UrSb{z7mC@V)o5eoSp8wp9+ zwB7K=d?Zw%bG%#!gxsth9|?6^@{#a7h>ry45N?)gjksC9^XF#i9n8(rxIH&Ztw7l< z{`B5<^pWoEH+M8k*%9({YK;W5^ihoj%BR#Uv-(&$5*|Ikrt9>On(l{9+jVcmnSVx0 zG)v(yG>b!lvy;`Ei)5+9J#)%@O9+1{|iJAY<|c8S{k@b{NO! zavW_-%SMz7g&8(fet)z$n155joD#xeX7;oaW|cp&X;Ce!T%=4#s3NXE>quq`b-A46g8 z7n!+K2_?+!tvSqxJ*X)v%OT94d^t>GFg;I1Im{`718UWb!@MO> z(9Z!f%pYl68O#gx#F$`uMKLMtn3g(Gc9HyPMFyns5rqkF!x>FWmUa3o9I#QT> z>D-(_8F*bQ?u(Z;=4{-%DQDxQb+{?2)#Ii(C-nF?f-`E%S7BS}{d8kJ9**~;O&iQi z@rAew3l~h`Nd9;rh@0X~H!In=-yJ%CS<_d_peargFndc-Q&b8?Q;Z8iQ)FOkDIT+J zu;83V^w(+hSEk2cQ*xFM{Tbr1uDKT(#|`^f;QywAVIK>I z{k{b^YzNL5T`0PFjD+(`Q~n>s4BM``VAxTDVM_r22MYY()f&SVBm94xuJ1ZC{x41W z--jnH{Lqy0Kb5;z>u_j(4LP*@#vIxge9R=#`ggD~T!BLCIZ!@Fx3&N+Lx2WJ3v*j? zXvex(1I^Z?^|?j&K^RM{ErrmQ)aKBxH=)nd1tPQp9}X>|0f$yWfVPJ#_Eoh0157pt zVg1@E0~oYjBG2zsLHn)+hnCyfN@$&L()Cfe)bueeLfco16W{|XP46^7XjOdp;GFEo zVFoqgFmDEPn1}pjn7e6PHNvE}dSP4IrTyjne31apstE9S0Ebz`Dwu=-hy8|4UuN^- zc1eVJxhC(yGXC^D?G^~LX>AT|Nqr8jS3?f%WosGQ4O+iP2dVz4SiknCehivxYXMrW zss|r6=g_=6S=oc>H?ZlRgQcd6P-w5c`7(1=0DYgDA42O}i&M?Mdb~HMHsFxU3!4I_ zMjY}D?#_&~-MxCL-Fa>_{~=m_zO|kJ`7>ALGV@I{4!KddHIT`IV%2qQdRC;=bO~(Q z?m!F9;hBN-TYH|+$qao+;1I@W% z%XxFd-V#=7%Z2s(V__`S^pXv`fTHtdQ_{~B4IAHAe!qh_O*~wc9KN~o zqz(R+>V#0K={+UTKoFl`IXV3(8}y>nWMWfpy4!)=bOY*g)77ZYP1m|1H(d`;Zo0c( z+;j_sb-^%kT_8-uor39}c*>^hO7FiB{Y zSJ#xA<#Ii4mh=X)Spw+2)#)PL+wab3mP&n?SqfSRW*IA(1)u-&=VmeJtuc$8N&D;< z8gFN=Z7?=%*Sk7rr&Q`XH+i60ay&TXih{5Cy)K75*_T7kY0e>ct1m-tcNN=BX5o&< z#SwC?NCvrRT>lRz zoyT=J%((#^=7@SS%mLKAliEpb*;AOoy&23_qH@^ds&uJ|jX2C5?W}}ZhQbVO&-h=1 zFsE1JJsaYOFgMgjm=A=WZC#5)wik5ZIzi&5`f|tv8q1L5X}d!hJ(k3FwUGBoA@}tZ zAYW1S?2LvS@_|q*{Z94E*mM-5$7N0kd45%{A}-dYey0&aeo>7>3$MwcmGt4zwl|ZZ zEvEI)XOpD~)~_AglR;Z44%+9gN)@rO0f%;1XQhg0cL|%GsFRvL;E2$6R^ia{>(KXU zHbiJ;1!#%h99omw9NLwpGPF#o{hT>^Gy>D^Olbc^Ac^#qqWr5xMDQ_tq1GDx#HmTc2myBd-Zt_R%vVHe4Kp& zn|>NBHC<4Y=D$_uYy7VpBh1xwa2~ABozBi0-06&~$>-@80t0WX#7*H>g`48G(Bu33 zWK$fc_j94EbU$%;KibXR<@0hq1XHvTOaW{B>-D%PO0}_ao=!fGO;=)r`Kbe%;(jIG zHD#_;X(l zvu=o$J-*}|jjJ<9Q;fniRpc;h)u;PzH4x^Y${gmQ>Kx`wFAlScutc@vFyGL&4zspa z+at^yT^USUZvkeC3Z|0)Qx|L{%YRWL(D9qoOQ~U8Vg!!TigE^s&0CTMhX7#!pX1~@}!aRP4&X-nF)6KDI zyLuJ){Jlp9?XCI<(^QAg-{;l%{Qc3B&)-YJGUP!yZi+D#xhX1pb5s0NS2l$MHTzn& zTHk7irpW0mukS=>bU-zK%hlng=-$f8`Mdu#Hobv4i+b3!oo9KT33;6k>`V2ipA{l+ zMv1*)>9JKBL@@$@aS1_*y{g4W zNP|EtN63nk*mMF5HacU|c16na$+E{6A@888t&6pI?;msLy+5}aH^l}|Zi>v(+!Srf zaZ^02&P{Q)wrq+U^nQvok?v<|AsYYbD93+9daPPfIawantS!|cbo0Lyur`5(Hfddw&xo7ed`6UQ@sSy!Co{tDgb?G0WvfX${x~+> zzm3%NBO2@ZqZIcV#cQEi?A`HhbY(cq^@7xi6{JoPVM$`FCd0f*+rs=SXlpvH{{Y?0 z|FM(+bFcvOr5A@;Kfp?ug}~>Sw4Y~V(_<)0ClvF`(oH|IiIh0J$3*^PznrH3>5?CM z!Tu2$0Y>-Owj14|F()yrO_CJf9u3tWG)(3Gm1Hj{RKLI5-OAL zwPchVZRq!hs1r)CCzee<;w70CemZUbpU!ZkOSc|DU4yy>br0%6tR3h_>PT`4HasE! zD-&#ZK>kMt%r59P!oxUjNSEpiv(pLWbe*BBZsrZ=lskaG*>&fX>o}4|5pglZh5RL< zu+CICZ05^^!l9+-tY?Uxs@(WyB7XpIBwDg z9(Aw@O|EOJGvp-}4K9Q1Cm`Se?gC1_)MP~?&&^0?rWFZWcnJ1i6x*-d-a>$Ypr;{&9(-p{9zZS*%z5=LF*$${|KB+AEm2E;%QXrZX$h`@j z?yO6$Wt&KR<|&i*87T$>z&VuQgMK8yZv71X_$a|empt4C3!X7)PmnJS**VR@=Jy=m zn!wCi0Y3yE#jCANpecC*a3>a}~DIV!~=Zi38Zd?=u6^wU1 z;KYZ)PI75(XEKV%1%Q*wB48F1PB6wsLv1kr5;aoMgHW`p&T!7?7)ky}1|i%e8G#<; zCZ#fmNtpv06iopk5aKcXgl+2$Tk&4>hV^)F^hOP5!VJfBhD*ki!KA{OCa{rQ>)AtK zBVvzGYL8H`$6oyMPx!L72Yz|lq+L(GG%TX~0BM>H;Z9&4Vi#j;?OPL_=>{^X$aD`3 z>Khal)F&viZ=c?L=7Upq=+Yf$y3SNcXE+&T$kQ3F8{HNH0yKh4X7Ne3WGIX$%%AfW zA~*$OKiS(hk%+e{hoEFz_h7@>XU;BmpJOnzt;;j#62Trx1_xal1lPg+m`=dY(S{4u z0i}V$1@16zT=SaTrY4J!pW(dm^-c#I-kQu7*z6S2Y;GN0vKLy1@WA#g2OH87sg~eB zEZ?SkW+;}2oh#Tsfp|@t=6NrTjj>V5w2&bpnX+lYD&iX_8 zG)*CYjg8+8+Qm>XO|zZ+QJ-eFx2ML&dDUUk_Lq${HagQ`T2CaI49;PoFw0~~OcHaz z!6x$_xs_Z$r6mm@C}*BXXl^{>{MKYJCciQzHZ*BF9-`oDX5Jzffo{2geP5Zh7_2!w8w|C!nV34PA&!(!PbXwPqI+`srp~OsoX!{4C_tRc)N;txVeQK+xp) zlIubZCym34fuR!XlafOZIsmHY9h}HtT9wfo+La*}>Kkd8LnK$PrdK}$GnxwHwE4%l zH;s0`dS-!}fg!3hypu+0_I@bYhRm!pFthv{!OUtyXI3DcS%LD*q9i0wE@`U}=U3ZR zIKQGR;rv>*{Uh@$e7`im0!`Xr?2~I2=2x%tXyY$>koonC3$gAEfxt9)=Z9ewkrizm zA%Nr~;XzS-qtu!sup3OWBuYJ@CSuB`p>$NlhmvN=C?l)jH~KjPJ)-qQ9YpBKH{r&y z^PqYoHDRTRa90sv!j9;b&*=!Rf#OwucvUaBs;sR7w5)Vo#AhGRT;c0(M4}KN{AhtMT@!M#>pUc$?StyM!}Vv$(7Kb zu?>pbr0Go1S8MRA67&b0^yuZN6ALpOK`BrAmVA>TmGCvBcx)#>#yD zp(=Ls_usN^9*<4;gKn-Tbn}pX*v+&}leX6Z8K-;ZOuVE&?dkZl*wbaZlb(K9ob>b~ z=xM!y^n~63ePSTJBXxYqeGbwfCOsz&W1D@Fn1Rs%-w_UcmNCBq^qs*jp~y8-v2 z`h;*ur#;1Nbbe@pXuV-Hkg;=*6QPDM=* z+fZfaD=iyac}Z)=(PYvNEdwG{2Oc0Ww;cX%kbf^me10mh%{6y(oW7Rqgw%IPRsIH%;;uyU*2g>uuS za^XOM6QKYR;Pb!0L9KmQLMYiIQ*aUytmF_|bP{vBnmdUO6*M+Dwn!Dmdf}3j_+D z`CVbsQt{T{p)_slfrrW>^$v%6le5U*w9kAsHc$+mRGOw5+!Wjkxrvy)4g%Tl_>naE zm^7Iis2pALz`{`mK@xOfwnd2xyC%7?d6~?GJ(z$l?4Of3>5g;;7Y3mP<0waTVHXLW zebj|@0~gk`6?I|$%!P&SMHhCxrR2i)?j}wsD`&Y4bzxfyqYEoaQ1&2>IX6^q=Wbbl9d9JJ zq@LJ-)JD|r74ZJXSFcT|-lKJXr;RHf0*S4pM3{Ue9#m^zP7%~!#9Htd(UQN2ko-kB zbg&NmMWC_GEP=yIOaN|c2uvJo<}bSL=KjJ(yvh@=Y7AEuH}e;jc5{DmeWq|#GrXz^ zTy@RNUl@N-`-|c!vcCw&E1SWUt0jNoUq|*A>C9iGOWwd=@)y&%zwo90g2@2MNvsG* zEl{B%I*GJR%t^#ydo9#XBKQZ%Nu;BbXt+mk5=nSTOX?)rAII^U5Kf##wu9&-0>Me> zz)6HlPU6Wf?j#~4CvjsJcM{Q(lgQr1oJ6d`Nu0LeFL=ZPqJkB_mwiSA_>2(0M|wix z;ly`@8San~5@L8O2>BZ*?~uh?BY*pd{^Bsk{3dvz_8>Y`n$3|aHxVhgiAdQ^kTqJF zA5rj04HUf=xY2GZE0is|wv%Z8;&{TQ{~(*99T>(OHyT^(2`#1B z6$%hs03p@-@H?&DZg&Ty>o04m={oUy!kuTKf#o!k<3ua_p}F7!NM-k$^UB84$~v1> z<^`1todb#6pq5QnOIpya@z|R%PO>D-S&^B!<@pD;Hc}}{-&Q9+N z81*g-A=DcT3l*824?&Tfw{nsD!K6r)FhdV4(gKRCh9bvk5g$@yOfVAjPv0wu`D<^H znDfbsAh$#q$OIzUuNarUF`4uRV<_+vxGO@-MII<$ydEgajf2rV@B7D5Z+0?40+7UJpZ$5#$Hc#)Do zdN1=|0qH$3X@K-H)-lo>gN;{IFMv93=L?|LJB0wlR4iYKGG5QaI1mcy3FBQUtO_tV zkt&?M2yA^Cx~AU5(%GSgF`z<` zA(Oq*8=eIj3J6CcnNj^)A0wEL68y}SuMf`D30~kQquJ6rf(0qUX^VdphVAnr&OY}qysIvUDT}Wq>_%% z3ApDY6}~}Z8+Y3&`74}^?gy?gy5p#9oe(1Nmnn(R3*{u;FMV$ zE^LF*42UB|LVnLS2s^iRNVF%z7bP!rrS4l8i+O{c>ok1SyQs~CriuP0ZF#94pG-H( zYjZL3q3In8yarpHMjKn?wV}2sKN|i{u9dk>%%zh%D2dAgBwbm7P6IaS{*59 z;{O`i$d{CB_$|uz)j^7&N$Kh$8x4VLl_8T^lZ23oz!+bfg-jN0RfJ6Js=p^>A~A+e zU<|#;7)PEXWBl+BGREL9EigtcfiWs><&05+j#xEa)I5mxz0{V_#RgM>DYvkmC=nhq zx&93>^bzRySYX`Ge=jiU(^jIQBmChTY8a*iMu{g3g6tb<9!tkH_hc5NXL$L|O)y`Ugfw%v&5Cx>am+ti~HMxv507 zX|p7vx1)&8+bW3YEqI9y9V4IZr*X?RWQ@!pKAel_Xb{n{Afn@?@v>zz7tu-5=wGs# ziD)`*Bpp3Aj7;LJg})V zrr!xmttlqx;ND<^IU!k820N!X%0ZV-#f4%GXfe`C9^Z7wVl|zGVj5cPJeDafRRzHm z4NaT)`jv(kzC#yT3HAt8D)=Y$b$WZD)*pp}cUZxFwBYkXLc#e$K_e^pITiHvgGGlJ zmPPnAwVM;}t)iYE+5)%I=6pwxWj-25eBu>sVVq0Oaq8Rx>sg4q)N)7Hmaqv4{84L- z%^vCtwHGRj?ETFK;?pibwNq$F=$WsDkkHM_SeqxUZGli*vZA&=P}^0g%@-G8wEJ^5 z;`lrU0UhU*xy)xz!hc7g=Z>cJfymc2%OfogBlYh63|A3$rdAr_nh%lIw9s0brg%N{ z&=B8g?aSjksT;-k&V{eZy|{shSWmZj-aC}pFvbyL2)nkA`e^XwPWi-_zXJLyY0@s0 zy0K_xp(I$X82%xp@6dvGUy_1(Qo#{&L3=1z!$5R&n30B&GNB-bk$j2Yl?qnRbV@?6 zzH1LM%A+9g>X5T#tVz5v@#>e51bcute^pl%L9*Y7-aNxc^5)~#V+3i9v_^@2Dlb5^ z&By{o7iJbg3gJcilXXsRb)5lbw-7+efMS(sF$f@)$K{Zw#j#+$_eUBG##W?UYfnukD6cUUd`VQJc#DABaa8D&wX_ z?Z45LzFo{*=_qXPy4scQ`brUx`uc0Zl}^S>Zn&W<-S-0y{H`sDD{cI;;Dhm~zF(Qg zquPIE9*=7L)u+az(!TtEC>}L?{fFaGv4sU!Q|+tw#-qxJzU!a0?~O<0iRCV>{ct?$ zd4PF5%HAZzqqa1b)^k_KemEX=Vl4)HM%PrvqrR%i13pUvFyP}{RvqxUM+AT7rLjC7 zHHlVMy{0lA)lI0ZC#~$~GV01Qpt3V#EySZdX|3nHc|5AHP~+9+SYy0djcuUD`(u&e zFMf{$-OwBu9+H@iy5|KSh({HxsftIH#hcJ>sUflboYg`+YErIDBa(U>~gc1+Y+)` z5rEoV%Ie&fe{ylD@WK}3?G+a?eoMk;_qwSyiz43ie=)zsYt8%eTQ)q>@RXGkt3I6HQtxk}@XA#m%5Q0E&Kb`= zg#4DVeiCOmjim}To;1Iu3CnLeSkhvCi~lMqza@1AnZA|B zeki}?&xZv(zh&fVwmDda&iPy4(sg1JnDg)qhq3FE0!#TV6+Kn?EwxwSWZz#+n(Ql= zEAm^?9u%%Qy!GuM{}w5SCv>?bB;3 z+UIT~$p3k$;vmhk_i)hvAN;dv#rycjGe@A5&}ARypB;HZ;R?$>z&~{xne)$>0D*sA z){*!}muMyb-1I>H*;Ym7pRZih{1aXk`Da-}#y>4xEb`Bu42gg2mlFPoO#Bf43^WS- z^JE$0pKgiBKT%t-@9i4`|G=Yi#>;Yqg) z(LWDG{j=x;{Nr5ToPP!=^iNHRe`Y3FtAE_7{;A0IkCU1HsfheDzMiaq94+ggMNI#E zPWWe8!Uy%wT~YrmX8iMc0@XiXWB)(!1^$Ua{d3>4{;8#r4k@ z$Up09D)mn>h5lJ+N&oZ{_(zys7WB_TrT!`QiTsng59gRWjiKyK`N$lLUnuCGMFRgQ z=GcVwXyXBOLI3=z);|mXE&qH{d~8Xy5VN{C2JWh-<@nh8bRHjz7O#4MSM|0W9~+U* z<6||&s|xU{K9=KS{^{!YSX7i8A1i^j>np~`W;^_!$k%ehe62~8KX1-ped~km^;gHo z7A;W3$7X&m#K)T8B?D;puJ{VO_fH?vy`}GdI6l^Lfq8ta)&leRSm_0y8Xp@z@Bg9r zSnR?N$H#Pe!sbrl1@DcIy?QQ`yD|U0@v-wSgmOFQe>gstT-%)MQwxOn*jx{Z>%SQD z;rQ6*`51iaTv{0)n^Bqvpge110P2aIIskQ)tRXTNjN$RINLra)X=Qw@p-@>)Ev)P- zv&xd7vN>Zc#K%6PwSHHM$H(rL;x)$58pF+M^n@C}8iT&#`!8{zm#+oB0uJdgdfoX@ zeC)4Ms`%LJG;|qrOG++dh(U;tMP8GGU%ytj93N}Kiwv%AIX+hEnj9aiRvqJGPHBqx z*x!Xkx6{_}(fC-`L;pj3taNWd9~MDMY;IW}RyQd0;gZ5u>%$epP&@C+q4EF8jNcky zvq5U#wQDZdhnvy_eHe)4TTya5fKd)jsy89Grngl3aMaxYSRXp1y-y#`x-Lk*&2vAj z4@>+Z6rMEq1N!ismpNw~ek?x!u&ZVG}R;`7c|` z&wtI8^kEEHC1l#}x9?mE-Hu%k=K78w{e*SAN zPWDwU?D?-$TwM19ye z7Ux*_eH#Cn@R2zdo+>>5HCNDwiaFM6Ioi0IC&YiQtMs8J^OzukGnDWXOzr~|3H(>8ZU1< zVsjp9lLX9?OcHL=)|(^H&SWfKotmWWGBinibz+jkzp7~G4L%nT33?Z}$G5LVffU#U z*jR7qOh)H*GC(6;=m3p@0a}IJ@vu}JpamLffZj-fAx2D1=&bi)7^5dH;23R43Ll$= zWAtEU#Tec6)=3znZxj_s!xSFGr&F8^Sch)HfK~U0V!)P~Ne3+SM>O=u%(ogoWMgT6 z#0vcp!TMuY7WRkT@7NzN$9<$fwxvk@5o^+}O%;ZyqCb9Iiv4k?8tD(eYix+(1JTkr z9RzD71LBkn@Gr*at1iO${9zO{Sj6)9{47N^J`)T6<@oG+A4A&v>6E=cmf7PTHdj<_ zk5$Qhd@h_NjL)Z7z8JN~#wBQvysE?=)vl_>XRqW>8J|CCRO9nh3Xac6M`?UEo1q?` z-sj};>6wD#^Sg?Q@#*l7I6iyL_}KW2?JSH>b$=+v=hzvl@p1+9xrhDH zJ@F&`p_#$Qr|nE(d@A~*^kVFf=qjW?zPMs(d>)ep1Qz<=f|0GS9bR zx2G!d?E{M{^X+}}B`$KFLUg6gsE_5_$DR}N?Vn5baLRlUZu1L*sO?K$LBs7I!3;MGn?0|l+JuRG{Tw?* zpxV!|`~_;eDHwOrIF~1m>CIUc)z+K%AFrQ}Pk!I}xy>nINjzlYhu6;+E((R4PyE38 zIi{TX`Z@VCVf|dHgv5Demjzx@o?&u0qD>*t@JIjPpq@kMa`e4#8`Kldd?EUcfe zO_0{lHOO)%(*th{!ur{H5?eoeQf`_u5BtAySy(^AncKz}r!B6Z_ZL>JpHELf zV(Mcn5mWOd#roOjsJwozIRUBZZb=0-IXxArDJsdr`gy!gASYpVnXjKylN99iD(^ky z^s)8x#)>${wqL+G)^pfL=9o(oC#ND41@WhtW91BJ<8MlnIkx1KYW;j>+`rvlLHdah z_E%n^v;cKZW}yzkrh&#bZ3UA}8VPr?6-;Ksc1CW=&%cj@J&v(1u)ETT&04KrURG(fJXMA@(@eh*t16WA8UB)Ww$fo)+-61FfVNhgzhodn&OC)G$P zvSzNH%-Hrv1hLmSDik**ZX6cMRT`_kd{#?- z=;WO7Z%2tBTM@hC0XY1IbAF~<2xZ4&u>a?Gj*2aWXYjie@Q>1Bz`yS!y362Rhz0x` zk|joFi;=K}pfe=8W5)+Fy6~XIxL|2UfM#yhbg{!r%^skk%>2;ZC~I>E3P>PIqiVNh~=~7-2s0W z*d1US2(9IZDPA3Y>;v~QiK5{qOzeBNj=MeJ1i9r^4K;+|Tqd6ZZBno`2Ac46vv6Bt1 zgakf2M)|mot+cZlFdC5Xvk84hOZ-grBC@mcpb|F!>GPOfdhk=ObaP_MAU(&s2N{g| zIq}9I!&BqBAlb;|uK@)q4&>J6lUoZquyF*LGaYQz+HvzJrXA6{mHUfoN1YJG2$y6d znl~C}Ftw$$A1xIN6wM0{KJKxRAADp-OyOp9R5f&jx3kgk;RC7R;ePdazl-pG@4)@8 zMzhF$P!KQmL9|Jm$Dc@XCYP3gOAkCG7v&CSF_SZ6WUh7yI`A0&R(5Y8r13w-$cE|? zbRcg89OW5sCRy1(Y4LD7i{bqeg#WbIlg`+aW8}Z+@f^wWCCH>U{9<%F0x~z(0egpZ z4f6oR{qCaAhZ?rxZW`PFj%O-3sjuQZMYH$7UK%|P~|J!7#s;A!SEKDbjx7L#8`=!)Sl-@-)45R9oE>F3ek?P~6?6IF#a=Vx>rd z;!>bMDPG(q1xj%!?gV#tr??Z`tvJOAmJqr8zVF_Da!udID$o@Zt@HLfXf zh|%%K$S3M4joM7vObyI`tQCdJcF`z;!f1Eh+MO`%9%jH_QmdakNO4Z_mz_R~7vBG= zioo9OJ_vWcVKZslx%p5$UBYfNpGbI*%ibEnK43J)Y5exONy{zMP+NAX;y=EOfaeU7 zDfbCB+lNrvrn>!+g6t{8%H8V_Mo4`U+|Y(-hHEcGAkZhc((az#lXs-9k2!u6%*5G^v0Z|3nP24KgR$ zIS3~Oh!S7ivz5Gvxks&t2P=iZ2Q96wYSA%EzQ9HcmWmKM4PWXf<=$|OU=0XG6Jhp` zs^p;eD@6Mj7bppZ%g+*A%@ojCak>4FX+E}}R)3EXk5y>wRM>J7UyN zgZm&QDcrb1eKB57-!1{m||2Z{%+5y9FzgvJU zK3fhW@Vm`b6Xang&_{dp4P6=seiU~q#{h~<=3K6=j+gk0+1KY@&!fMZsm?~$_k!T` zf*mej`4lOybQNffi3)M1Q=A+(=^V{EBv+(aK!CBMm(b5_j6yP8&?KftEcigtWW{p< zxzwM1A0S_2I85ikktKwWSRzTL=5{&xyKW6IfVi8u&mSt6iJ_~p1ED-JHmpM0m-ZHD z-&DvynQdV4YxG5j{~5AzEdE-4bY`jC}%8{2ua=)MXEOLm#aN}GK#w&jNm9xFDG)04* z3j4y5L?-2j_*P;a=Ri|kby*A>;#QR6dp^O16Prr7?9re$zLF+8_FG(bu4T>CX%1WI zN1zmFXRJFw8uZ1VJsgF5M5;^uzEj>+w$IKnDp&K1yY}eQ8rD*sk8EF@kbJ#TQ+gQW z)nP7q6n^Q@mcMM@{-*rjKZfBCxtcp3x}1i_oMp10JUCdsZ$z*w4;Bz&M)2*3)Uk>? zThoOET$4D>!Kw7#Wm<0$is5iWHwET+voV}9dq7p>RE-Y!l(*%S&`6I(3VeJWrz;iv zuY9^n^g18}&3K*Hcs>5UtEIDbaP6KjgdVeP42If+Vw8U9J_`t;xqtppwB`FK!~#yp z(X6|OEDVtC3+*H5?_LP?wzv24FKge2!0pyd6G(O1yT=m1@bXvX!QN+g#Qf5 z+YG@4O!-OAQ>rfgfV_gM3ojXd6Vt)G`?%(*-A9vwA6`i5{ZU@v;-1>a)hEB$^`7B& z_xBHn(X?sKW0+#$-yPDrNK0 zgdnMVklOWw$u0@1`GpvUB18WN$}#cs zBt67wLe@vUZ|AnmjX?D8{(MLPO@b!<%{3cHbzdkvpd|^ccLNO%kRDGG@%JtMrga>K zS9vS{3J|u;Aerh@km>5P&)O#FE!3^`4Q&}W?UaZAJ~-NBcSz3V+El39r4X=;tCm7} zlpyldSL5UA(B>1GVP?2#UQHSIh~xKHs_YB4KG7~Wy&Z<%-#$+x0XmxoCbgTV`b4u) zOQ1MEg4uWucip<1s1E~c{8a(oP1)O-1jg7~Wi4Xwwy3sYy+PlwC~@}}tl8V&3Zi`G zQYs@hFhVS3EwP*PSoH6cbQLMae>4T-kYDqC?vmEfCmyNSI(sLlR24(2JD40C5@kpq z&TVNMqOG)WhGm?egGv6E1r=5i5hGv5;&tX19~FT`o`6xS_9ZG9J5p!%^M@jL=6asY zSCOVvd1+096}BbpZBGK0kyCViGw-U+Z zS%j~ijL{1}VSg?T?UN`@B%Y90%+i z)-Ms*j_uQrqfvwjT;U*_B*8gs49T(UTf+(qQ40%stv{1R;rMzkyT1>xmIwv_FF`q*_kNIr9^6w$VZ^$>dslegFR=TetRsZ?8x705aT%``? zC%+6Qfns`c`Yz-<8c2}2*`cr~7Ianwj@MHa^S4gq>}~vxpyac?IgxCw4Ivw<8q!_H zY#()Qf-&#Dc7!ya*gDm2U$6D(FNl)co8UIOpT1RB4~ zIu#$#p706m$Fpk0#j0UGi>&ghpec1kZw{UNiT4uA?v3qRV5onH!79d{iW;=Z<55(C zL|)ZA-m(}NJ~wMfL*VlE)27bivBo&~)mZAdbHc=S_)Sj{JbR1@<1F8Ue6O)uU4!_q zh(y$@+RvPyskXVv4%O8dw#RRE?UGv5$R;$$LKAy?L^9q9d)}cf>Ym8zef;=Nq|ZM; z0G4Mj>0+S$vC_|iOvnb%jt6>fk62m2S4;m%+;gx01dWV(Jh~~2g(;ii&0jKJ5dKys z1q3}1vvD}jj-;GOLr)-5fmyC2;iU_TdDY@qp`xM;w#esS2(|vd$Q}Mr!G(|n+xUQF zZ3(3hP{s<$6-PGE(0-g=db6wzg`EoO&(kGD=YiCxfZFzDsJ8TqQ!G=j1*kg9*_O9*?%j&ExZaOflAEW_u;1seXx; zTdD5N^Z0{p@r;A*1AyB;)Z3kra)#*cZ$8QdX^H}fxFK`Emu|cr|9lyub8l!Odz^o= zP~_ZozNP3!WRpdk&E#Ams`D8qdWF7|-CPdeeuaCvmKh;8v3<1v@S^w&!n1)7E!Y#i zmF$aqNxq((esc#3E?;YXFmrE(u8R0g$e`Yu(W6lB-i=vK$mPY|I|h1xwcXk8n?K=F zn+9%3(+`9m!B}SEQ40opRUZQPI-d>YE9d`ehP`i*3vBJlVi{8MzbA!U@&jY{>+NY1 zM+|c?Z-^k50UfS4W%9~ZnG%`91%VSsq=zfrW74RL+M02AXYFCooNp_|4TM!1!0EK-Ijd^K1D<4c z@U~ZUcCV(+;Pr6$o?h5&)dk^@EwWgMe#$Ki6!J>8Gc9=*@~8 zEka1fgYfVFSlZnn5a3eO5y~U+p@t8HYE+R%l_t4u8z(Rt@3kkbwz!JK`XIr9V;qFE zmgi7FuiX_KWKMqo0mfBA6b5Rg`gu(sCnIxE?i-dt$GI+8l1JxKBmim8Q7kaZh&30* z8eE%^A^bBJHP842Kyf`zH?r@*LAAUf6r}RKCsI>icXVlOQ2#kp?Z4M({O!>v*h9Z+8V?U&dnuhdh(5P$++a1$R#@A9Lj zix+<;@_Z5L`r+3ZBUbqA(Xt!4m9DoUUE#z!a1!vHTL91SImq|&?BbpqWr!HfWpg(K>VhLmBfRWc9pa`Zo$D~{tCKCLh!<5hs4jh)5-Yt5(#VACVqS;8XVUeVvN zMOs#@NhenG{4Y>KR;EeZT0wOF2#B68bNI!5yW*U9M#BdwrcMQlu7`UsFgOWz6ubf| zp1Jy@z@XbR&vD6wG`&4{Lw`CDCS}pJuY%1z{L&H$cb1|9@wt=jxz95XL3%slWl<*j zRG@!IEm;(81r11u5HWGxK8pXe7an^H1$}$ooKBK|q1*TUkGi)b@!j=+1~(PpWA(G0 zwHH=<^DaeFKRHDsO>EM?GFl9>O|M8jzfCV^ckjhZ`OAoH@$#kHd*f|PNWal5Fe(WA zZjx);`>E?6nVASjfq*0J#C7AsvP(3EBhY zujrgP`gQI5djlZ+l>Xkp9G=K0=S{|>G-VjCu;WWAih)D4O3ALfiZEIGAZ2#sI$^I= z=BV(XjzxzVeayKsORC&wF7cJ@k=JGwF>Tg4lYffed7Q8Qt2+G!PwZD2*?z6%)SP^l z*tB0)B7)PmtZ$fjc|W-JipY2GHT{aovo*nX8@hD8*H|MvJEKpRL(WdUMip^nsK35J z#Tdd+BE+|-ERS2%I^vpd%PYTn)l4tdY^#z*hRaF+Bd$C6!q{MrTiP^oVI(KRrCm>;p)j{9$KC+ zeuO#ZSO^apQXPFBSHrm0t$i@&Q7X z?=2Qkz28as%ZOMo_bp}a)r^tT2WcZP@Er-0R5VM!8 zugh)<_rd}bSZHF0b-_dV;rc3I)KnSyeU+zou3K1hMa_X6-QUiIY=J>K35Rk0Pj?gk zQ?}zhMWMf4_eEoO(^X(w8in8cEwwDS%*$fLhVh1)yFI4O$DtkW*XRn&nWYb3$c1q9 z1&7i;_}du~CAg^+5SQD2&b}qUbGxTWQbuO@a15?i-ud2WKJngYrk(GDwj4$Q=yPL$ zfszEESN}Z~?&s_7)~8b5vu@MTK_>h5rQ0q!H!J+%c4Z~?#0bAuMg&pobbt; z?+TlXvzo~5oV@c|u$o$IstuhK@>*D(>sy|R0$x_0%c<43SJJ)NGhrClexjt=`epsv zYXv8Wt<%T{sEn2guJ*cjl_>UheE&6Z%Sh3b?bz-ElvH)#`|&7K3#}x21rGO*Da#lE zjBKsquoLu84BOJL$M!%G4d843yw0rcsgzd!z3)=HDvloBLLW46vy`JB`+G!QK8Oez zbGPt?pX=|VHSdPH;Q^mvZ{cj zRQ3Qj^nNVKUM-3%E^7FCL;YTseyo%a+QbGL-q%SBu>Y-TeZ%!p)@GE0F^J=}qo(Yu z{l=((xCAii4VS`&B)06JN6@}bbU;Odrtgg_E2Huam%1zpgZn}J1X85(??`2@_(;X- zT7`7uA4dDriV9Ip;LFfs8VsDKu8a*n+v$S~hU@1h?s|BiJ;Cq!yyHa-MmN{v8a6%- zP5k;}y{&y!A|wq`t_%NL0INFDPK+>w$STrfchyoPcVb9&+o{j6%m)k=Q9 zXkFkz-@;f*QM`7T$Y~l{aNC#~!igXdpw>{b3yVAUkK1}(@LUU-vbZi|JeM;vrO8%b zs6>hNptaY@>3tZdbP!vGmKsdRi!T9mBKkQfvD{z>JovRI_vM$!7`!kl-fL1YlMU>j z`{a)ou3wvqRm?&KRagH*RmY<`=F-nACGZ zE;381Ka*r%P|}*caH518_k%4{5t0X#6&Q-y9BmuR2yS-o+~3HjZHy)`(IV67RaSEC z;^L0lc6L+xgz!)3R!1s+BK!5P(&GMxEba4L{;>QjQnHM3-Jc&{5Z^~Fn1wCWw_=l9 zgnoZZ>}8J?RmU1`O?(ft;qQNu8O6d&zIGSp)m#6GAbW#_tVmev+txe4e+Ql)jdcKm z#I3%6=|*1fydKRg)p%EqMIcJb&6X-;7h1gKI(ZpDcnXLP6NMBDkZ#Y`t@;+@i&#Na&4=C{8ig4=^CyE_e>a%r^Y7qbS~%@i)P}%a`7UP zJZD}880Iedl(W)((R80UZPv<_MZAkE@>wv|H*{4In&;|3SPHeQi}qNJhU>ny188%H)UTEfplEZDltB$<5#NffIQo0; zaONktAXP}2e?PFMF6(&Zl0@ae_dNTjoe#^v0J~epOK18A;5Je}>zQQ05i8r(>&g64 z8dWrlb>GTuF1EdWBX!A{4RkZ!t)K|4l0oegT{!1RFkLz$x}Fd2Nuh1MB1Z--y^G#i z2M|pf)h81X8ls>R>EqBN&?MQjg30^VHNt`;P}2|+=z{;col*UUvOE*i+A)nzh)$k8q}=sz5AD?xv@PS z6yeV6%<$|agw=g=NK+hDlt7fNH(BrIG&5{klPJ;hH5CSb&Rn_yCso>ijYe=p|ZL zPcPpE(cdAhkh3B_j3~M+{zKsM(;JjWCMy}Td<*Ll>n3s$-uS(|uD6HHWET{PUcC<* zdrAQIzqLl%H5*L$#?cHN6D8+@OM;j|sar(fW@W;|ok6<9hf(=~ApXmnN zslS3U5I{#Izd)*dXc-0km%)vkT`oM`W?Y4}J|m7=J|;tLq6Kcie{pe;8Lgl8K_;HO zSKw3c`)uIPRqJWMy(c+5sp(n=I+Bvxn;h(Zw#@x}^gr=@UY&(!e2(BqD%K}tyW*i? zIW;%(RVAeN3cT3*{4kOMEmpEy8TH6{2Yy)zJp#42z1tLt{>SM3E$d%I^UnKkst9bShyK^4XwS?+48Y_7I9)VOLzKaKAJsyo=;=+Z@rBUZ~ zg8=`R&)B1TEyQ#2YryH$E%^IBD3|gpqH*gPRy1|1$=44k#R||usgvAk(hmcav9u8L zFP>04zB{00{ybob;SvB)cBko70rYx{(9tOe4!a_BXrVSo)Wt}n(iwmV-w8;0eBepYMesJDuCQ?)^A6?nYWeQ#i!)~?O(x#UQD3QIMRv~DvAJ-TY`hdGS>7YRgOI9DTAmF~a< zHNd6x6A=94T}EA3!MKw6*iXJ-w0Kg6blAwD}6Oy)Cdtwjcd(x~fCQ z0atZ8$+sE*Ky#C#=9Y~Hi!CZwM7Nsw!N8uro8>a665;pjF;cH!Hrd7~ANVqC1kw%T zkXrySJmbztl@NQ^%t#3f=3c0jATo*TS~JiAdUWosi*h`G*@R^vSKmXAepUhXpPgDv zN0zHTmVQJL{sRdXAj_v8ORa$w&wHzI)rao!i0L0laRO#jX^he>gxTQE1N4$%HW@^Ir{q@6&!r7g)LL1ILLK*f0rqz>; z>Y94wqhk>V51$4QW{mjDr&NNUL4w?F#=6w}CS1i$|EVbP$Uj_45eBEWb{fdZ63PjH z$}j}_Lv_?#Ox~c{1Bm1S8x#QEX+!8x%2z>mk!~Mp*YA^8HC`e;fmETt_$< z8T%$HS)3q${T~LMdbU2$>9AoHoJ(nsN@aecez?3@ElgJqLv&JgaJ0HcY4x~U_i)%) zVw%i^sA-3VY{|>{>uagu-V@`lVPobP^@S%(%EWPdL2t&$9&!%`NkKvD?1DA`%PBfw z$3$AZ|5}}C?x_Z7zNozAYTogLW2F%NE&j(Ko60l|!i^p!t0beJ-|;jyI05z4y*~l< z8EzP@FI)ArrR$9Xl5QfWyrYXbM4d@wKpA+gE#rr*fp*wJnDliCgd`QB^3oq{O{`P< z1T%G0*VzYMKr&FX)kpg_I9X5S0>~$EMx65>Cb)Z^;~#pAO?}ZPNuoFM+$Sj#<&1M) zUZ&a9*Wtk}%V`GH9a}TqLcmPupH7wd* zeyJf1{`@3dPgyq=r4k_8_@lK(cYQMH!Q4YUk^RP_?ePIGGm{6R0lS_2mS|y7iL>CXj8hFnulA$jLCg9o6W4$qovcGL%oXDLjUzolIN6?FP(GL4^d~YkYu4Ed!$lTM2t(zNF8V7ehd?|TTmA|#10~5jfF9l3vFD@r;qU~)bVM4&hE-nd5|M6$5EVr*#A?dLIN1|kt$GNLm>QWHY}Ha8lsxDi$4ClP zB-(Rh0J-=`1g3Z`Nqipmp!t#g)Go8Pu{+vMx|mICter5iOvX5uU{-8OAdsObA=dRW zZmjzW`zSY#*t|HlJ%HGy%cx1QM&JuRQ@3i?!NX?X<-aSq?5?KffcGQ!Ia_hBZja^* z4fS#Z`NJ(+_gUodnWC!`Gxxd_tDvYDKqQPI**fUMt*y6ToKMIAU-G`=h%GzIlTF82%Jv5Pi_ca3RHpG4jXx$pbT z47|Uru5M|&VQRpEfseY^#3ThZDr4=_@q>Z+8@|4CBvU1{(KDv1pog8T*@P~gI*q9 zYXrGjUn6p@LlV2FZ3c6Rq-bp8-NA7p7De3K6RPyX;q9k-^!fPSH9W+#b9)qK)k181Sf%}n zdety(CUt$eg|%?pk{1b#dcl9XZ+RW~u5bFY z%6f!>>7UAuHp2i$Emxbc#2Qz%66lkUkr?IQz;_63z+K=wRutfH`bAYm@#!Atcz)bS zf5%HRTy@?-3UW-$Aq?F!4c#|&CW>U8r)!&GVapf#>bJ&N^-odbVvXNM*8IT|Sa7rM zAn?GGspq%zsk01}^Hiphkc8ZfLPu~OqXsLEpYa!!JmVQGYc28F&9}@PlIXgxEJI{j z0JqgOoe_B)%|gD6ztvuv@I2dMB$V!&+YCcgUZ7{D%q~0&j9LHl05hr7!esNpbFQEU z)MV3jhq8G?Qq9ItFu}>$Anx$U1ZFke=GuPXJ1^=#mm8ucCrd~nlb-I;v zv!EyAOvoxX)CfypN7uK63D8bQ_VAwshU%K-#=k$^grihUn^lR88Rmnm>vxW6lfmHPQ*3IVb$qye!t(kPjZo)EL zwZqG`R~}ykHUkTn{-gVbU!S%&Pk+ZLTTK*vC4H43<;co%C1~i$sP)R00*g65ID>Mf!jQOjqs(EY6fiY z{JISm`h(>)f`sRNLYV-!{*gGU$t+UJK5eV)+6dsp*OaBA`dV;+J)P(`N1OSo?Dq%C z$p6fd+kE>IKqtE`kbrIF-I36mJpx+WLG}bWpF`Ck0~DUfV34p*W<%+jjN7By2gQT? zf9fzumS4WxyvH{n^L_7l`Xbk4vw_Hl4%nB*)7M2n~H`Qs`kXyE<$0)SIT z8Pg2`_0!RNhu`pDo@^E`+Ff#UKPdh>4P0poemFWBRbr?X+578+VpiN@B)Cc3&!_Hn=aLqT0GBx>9yagua409)!u?A z&=N48AoIWAWdKG1wm#os3x*sJB1d-Hb@kiPUd}v8|1TJ<77vFOXt7I?-cD)EwBA>7 zgqT&es{m!+R4;*QbM9MjkK&7k4!{pr$}d(Vs-JI^Gjkj8Rr>;wTawlK8s?F3-_rIF zeg5y(bD-LE3h7JWYc2$IGNIu?CWwA?J)+P4vNMBxHWOMcj)V^gfAPFwTLs1Eu$fGb z^QO)iHkD8;x4t;f$i##xpBY!knByz@&xwf~fI%5Wi6E1!7aB6Jac*=3p3_9o@t(vM zJmaZ{BlpMg2mhKEMq|IH($bC;51AW+6jC&HbVx z29B6M6w)VL`X3Y$^n33&2{5-4bxvnLLpPJ>j1NaR|K?)41k(Qghj6wb=-Vi@Q)oe* zH?Qb#U9D)oyhFA+OZuX+$k#0A$i{)9bqHtb{A!P;>Ym5Shu%IZRNB*aB6X|y(>DAH zDF(|uyzdnmdfHwonjJ)3RyN%%_sVRc01Cf**ZFihQBExDC{c|q6h*}sukUFMy}DE2v@n%KP4&_up}8Otb&5f2R+jPum@5 z;ez)avg@e({(h>7N2R4gk2i+9;#RzrgTc2GY4Zr@pyyRU;lQk1Q6}8?nhWA#G&RO* zFn$BP4FoAZa#6*sK&EC9!`@WL?iy&>D@7qAV z6MAGDAsy0uOOfj_k>^Gp9=K#-Q16|1DQv2z|S3Vgjrbmp?-Q3tsQHt_Qb&ur*#hUc_K~@oO~mWSlp3_Inr9WfB4=&Nam z8axQw*`;7T{C#0_9_HCA)a(?5QbZL1Tv3WGg`6*qyD=BTt6zTaYTohBA#?5qVl3L@ zG`G;UT`#3wdT4if($_5F+n)=&>{a{cQN*=wN%5+jxsGhs z9Et|9*k1(rc*GdHpKqPGblpv0ojiEN8DZ+(3r(v=H;h67LGwQmV;_2KL-l8G&73PX)y|52?4GG*<4XTygT9ZIaf%x)pHArq5^AV*? z2qf=4KlGHB2--SUA2HeHpZRy!M{SE5EPfSY2E=(w?QBXt9ohQcJCx{TN^D1K+8j;W zv^`5wF*9;9*3t}P?0V^si$+&7g^*|lHSV`zGcP?_DOePhKTUGK&uNKEM62vM_pnLr zRN8L!d?)vAwerh}YyC}HdXk~{w+X)}xZ+y*D}#5dhqj}b>YV;#GDMrD3cQArVc>FM zzg^rNHgU$+$CM^9L&xsN!ePSpD<8OC@=IZ-!I(T4F^L{kiklWBb^chxHe})s{?9(Z zr#Nr8dpe!w`xOhtZgmQ7W^AkKL$S8!F?Y5K!>kBa+dT7o%{Er@xB$@}i9A+%NuzYR zBog+2CHa)*8mzG=zI6=ewBHME)JTyJ0!6O?ImgR!U((FxP@1M6B)Jl77#RNfM$%H) zQraPR9Rs9kH!L2wnWj1~h3rZ$n>M3*=IMM&1#fdG`UUvQJ+N-%6Ou4{A9r}h1Vn## zpxbSR>Znu?5)@4;atfhR$TsTkq{U;AR-%8`W;*0=`?euFF-(4Twvjjbj$Gjqu7=z0GeBn#~y_;1cC0 zepBaD!t!CZSSB^6yUPC8nlR#Tl;G>giYb-L(E%56e!Be+op#vgb^X$27s(=-oIguA zs$HVJrrxoLkTD~NBf95)u=45LfoeU9Zo~fD^Sle#w)f zAWB{4u{E?Fs30$-O+iD4f6_wLbg1SulL_-?t1dUDG;?C=TBCv4Pb3>d#yEX`y36s$ zL?$YKP|T~KuMTvAjMI;ti&?po?R*@ zJki#ZCxE5E7Pz|gzFI({RLbr4;9f_?+`ftUmYp%9U~mCPxKu!ahbeHDJp!i9PIPPi zr#DZ4wB!>uVE=^6m?OJpV!zh{L(_ufSR=G))1#;j)}YbQxew) zlcsmG))5%u`ar5^>|jH@uP>N1l8q zFNR#daq1f9m0f)>%%egPQm3r0ND_9+N|hyr7XpTRQ04Y9BYGA;&O67czYNWyoj#z^A#Hygw})>Cio#`@F2cq<5Vdr)R<*M? ziYh)wdvndbvI#QBIlbAKI7mde$qei`>F~Di0D4+Da>28tycw$9E!%;V!KlX3w|QVR zBR1inh4-W^IEa0`oH#AqA}n6^hM8?J7C#P=1K%A3JrX@o z7HBBsfR9{|$3q01&i4B~azOhLIq+0qURNp6iMqlMK;a~n!Qd*3NA`BlmYXgYnp%0euYWWh(@@w}DjPC1uv+2S!ny4WLh2qbyAmG2cr5>`*)Es>U)dtnT_+$xjRMFY@nZm$cXgp1A4F`wghYf&)^X*r#ga*R~F8;(OXtV0WK-+qP{3 z7oKXMcc0R@ZK)xD`I1fxT^B#K2e$Io#vk&8a;tY(TLgM^=_iX-zk?o}%VT_!U7Uo& zjz>-Eew?hywaaEU3K}!gW4Ye!#V(u{;zsr|x|e0N-_61&;Y9@FceCm5Z(zsbu7OQD ztW&V#+xurx!ENOAlzL6PhMSU*Z0UMI!-xpFHDeYw;%UXO;>@nR0mwCM$|y5u)-3A> zea-0~-V)NV+{IiI??;y0ziDChymd4%i|=NeteEG{U37>~g0_iHgK}somX))Pby{S{ zA>fAE_ei7SZ*_L;&p9d{4~4&=sqg8qr*Z(Alv^5FKFmv%tm5Ko#hF_7Y{k{|9=x=n zh+IYEM9H9+E%aeLM&XYTrBvwZ9)Rw;k&I6^En**`HhZ|NX%6R}5TsE_V%gQq={I0&;2`%?JGq-cH{_}uC@V>fv9N^wXQ0ViC$AXNo zyWk4!C!vNkdT`hQEz%TUfdc}s#Z3dk_CV*Rada<*JDQNIkvur4$$#Ymhsym~f0rIA4 z92c<#Nv7u)#XzIL?xb$)(Y0addyno98?8yhalrf>@IhujzsOn{?znLa25`BhL%lE-+Vz%e{lMqOPOs`woNzcjF4z%0smwoduV==qxe#f+k_%mo= zucCwJ>*i@{E0=1d)~RUq9nJ8X1ZXtPvN}o2!RI|da#ZtAYCL2ZGn&2Dl|{EljmMd? z>wT%;jtC3~YxeJfyffgRa6@`X6A2ou|31)>I7dx8E%!H;fAbipcYc;tcC7L*g=A)L z=3|AaxTru|%&tKM9(VZMbt20G>OAUJ?I?crg&srQ_hifhL72yYidd#9=dqH^c;fCw ze6H;S)~9Jx%Arlhe8*BOYBr#0a`&W2LmIX3C&O32y`r+!yl7k!g8bV=CX#^pvBRjC z0DebOi9ih(xo+8cHp1G5u^!?9v&JcwozS@)7Bk9|YoCW;f>{sR(e%p`e9~{lVThNk z2w8UJxJ{UET6Ie6-QWGE!Gya!(Vtd}G5z)S4@0N%0j&KWur56X1!>`2+K)+maD%V4 z(K|60os%y)Cos4$Bu%l9t+-NezLSmOFwd_@?Dvuoy!DCUi~T(Iuc?})i$6-t+B8`- zCRrdp>fp=JuA*j3iY{-c6!-n%3$ji^Ym(~0w?J|^>*@Npx32jEL$NH8lM~euxHWBm ziZ&Tif!$`&)`Wr_t%F)a*M5|Cp9KuZ)g&C~mQ>Kz=U-zuX6>X_TGS4`RHwqzTo7nS zV<_{^+u1P|!dfn@IomO#e?f4uCm9l&rQzZ7ki%Nlp;rAQWEGoHUxe1A6ZYH;ft5CE zAZBWj>hxpmX5MgBM*Z#NN(ox=X*U(MLto6|GS!4;tZBipSD!ilzEub}NXV85Z$g z{=?(dOnbjl#9TMeTg-thg@RsxRizCgg#`;@yId0xNrco4!+7vkPvbrjllRKP4PLAH@`TDnZ6Z{l7QF z61`qr)BD|b{Y`W8#6nvwg75d7NQx1@9uPoM)@QK?Fy1ZTr`sx`F7>ttc;Pe-(` zQxy-ULE8L*EyF7teYG)|QxPhO^HX`c># zi%&qv_tWo_V`69Es>AQRN3<~3==O{ubdd!L8?9)&jG}|iDVQ=;_jaAN zRZ4|tIHilKJ!jck^Q-vm);VhsiiWG@26x9iZ4e^iqm|Jmt& zjvhf+Tla3@WjkEL0PTo(>=9Te;9CxsI-XMQ?W-Zd1KE_$k)P1S;s6E6DYV*#YYFls zTyiPIf$lBS`&7?p0*)WU=1603{WW~E$tt=;;VFbd{40lW~-{QBrloXyT7PqKjgeE+c7Q8ELJ#VZ-~*7{CanqTnD(lif_0w zZi-=iwBc3ndGW_exvbgGv5HQB!2@F`I#w%)%>l$^kQZon_xjulq(?4rAA^YB8*&9^ z&4CW76N>QSL>L8ve$$iR$BK{&5FAgnU-D9&X#PFiogIwTrWROw3PEn%rQRO7VS0A? z6cNsY%q+XwG%>$-t@wQS4Z^{SgvZ!@NwEGz#`_H3(cx(odVZt68hte&^lHL`P(vvyc^qzZZ$NfUXNPudbjiG z`B&W*!90Z`lRgcY-j`m^B|c*br+Az1f_zgmahLsYnnUcW6{mO?t$Qs~|50=KmYU}q z(;7R4@8l40aZG6!V^>9>JDXEv(sMnPi6o0f&*YuaFmp!pQ(HJ7Bt%1>U^|+Rp9Ql9 z!71pOz1@B^>)xO@vDTxuK%$RUxbi6kXVLf}kCC!vQc%lFp`GTS)U(YS&AjdABlW(RZuCV7S0u^&O#ML~Go0$mTW(D?*g` z@5-0>kW5O;6yzqPg)yyWWR~T^$o8BeRa}Yun@~%SIT^Nxp^zlQzIy6U zeZR``KJl)^k<&pqxDCaBF$VI1OHSN7>+!X#JEgbdv@uiXl2E(x}?A1+5 z`qkPiT=cg%-r}inEcE`5uH<-mpj}nE-YA^41WN?O6HBx|ITDg>H>4>iJH_<4eet8P zWs>N*?N!e&>t%X)E1X zU4yDHlEyDd=+VA69qHah+kp{A+S~^sL9Oh%FF@iB4LUz(R=fm-BFhzfN4<-h63Dy0 z@%;KA#39v84ltjTHm=Jf`>^R4-6El6ZJ!(Mf)+!tGLB|u9#!@HHl2o=tE@Jw7l zTrX5n{SB@mQVovT7AC|t|)5Uv3^5hc%X&( zYIb&+JhkZGqNnj)vZT<-sw+PA#9KEDS5g`tc={9@L+kQ1+o5$L!K$=F*B&dAB`>|i zqHflVt+4lhAM6c>2OVQ-3za@G=5?bvxO;Z%#fS{3`jDTzsFEGp$E~w(#;S8p?M>qbX~3?ljhP*hCp4aeHt!2eQ~?)anD+sxS>J$%@d-lxwu`wbJxMG$>M z9@pd%tgCm<_4X=8Sl-Lq-cg#y_uL5)Ep>-R#s<9={5y{fZ>jqvxP;JDB=rQEeT@#* zzr4;L*@e6m$KCv7$kzI+-TKl)5hux(MX8p>gV?qgPYIe0;vN_V{`vaGy+603Rvz^U za{4V8xIQe(dn1$CJ72ZSyS)*v6}?#g!f17u;S;}P@VFFKWS5-smQX+A@Y|)p2v>bS z`c8eCpV_xQ90SPD5&b)2_g`Rd6=+a02sE?stgE+EP&ZX9h*>tpcG$3AR>Zlh)$L{L z2R`e%Y9lS)=E}VAM@;xQaMr2Q7en@;H89Vo-qPvu#Tv%!3vN7ca=w}@B6v!sdIA6P zi3qQ7V7W({dpbg7j6UsaP3HsR($T&`AeZPAxY&C0LiwI^xb&UbVfisnvN$&dkB=Ry zsOuKFSN2N{Ry#gl*+k`+wkd_-)MREKwIaOQbz4|od8oYvSI(tLz=04{^*wpZTr&&2W7*CN71YIMP%N^PAbyORPLO| z2^vbZHNUSlkzWH2r@^j|~C>wgwl*ZutAp85^`$YgZpxzb?g(a^kB!|TQty`{`g z^S7C!$`F%THR;iU%#&m<3oKXYNJn%6z5i*g&4m?NJ^FAJKRUas@^Y+8kOK0TT8;Xf z0eWuylKP{7TXRJJct@H?)E&^9gjbsEF4C{j@}15!x_;J8CU@Qn&>CM7MXTn-#Cl>< zIqLt*Dks({TK|9{Z@4{=ysAS+;hu5-vmM#k_aq8Nc1xE63o@yil8*0B+AYXbYM6k? zm(L@aACYaQ7{b)aPBk*3s!M7!d;RA3mFUwC)zRKFae*y;)m*t>0pCnkpZ<|B zF);I!XX|+%|N2(i6GfVDZ6AQl>w$9dnl?UBl%76G+~y&ED@Of=srLR#-`w2R?Su29 zY!-q{^&RGB>0i+vKbMW}QpBgJtVIRcgY-@u{6jJ z-C8*jx$H74q&7c)GJ z{JnjWKPp<@$yJ^&AhP`Nf15`3(?+`eH*91D>vUR+=og-GOGU?dv0;Z|mN`lXL@X9m z^=6%Xc)`vnWh@gDe{^0xF#NgCAVOTWPiwK$(>qPk6E7*NLm2S`I=l~GOTP;F?e{pB zX{2!1%s_CLwIPn#MxSbOUEgm7+;YWQ#;_$lVA7qaKnLB9^-+Ekx1(lt|$B>HHt^r6>_($g7UrK=Z|AeS1OtPMi4Qi|l( zoQ3i4R712`wc?Aq!lIj zHwLXUDi-Al7w+%YEeAyXV!ZAg8sHYJ`#Wjk0FLpJ(G<#q!qzoMeNsOPM$8;^Jew!n zy|N|hj-h`|7lXR%@wcLQZoej{ncmR0Cm$ts=`qFMFJJX`<|WIayC3%9YVHqf^7WIbyAcD(csqPR-nXD7KsB0717J4b94eAu- zM$=1wQV}oj{KAQ!pFELEc_m7u4nIQof%Otpazc-EE=@3y>u7^J3aQFlL>-|Wc}h3E zZbOEjw}WZAoMk7DUPiG9f;X2 zRO1D-U;0vYC-@87`mQ#Xng|AXzq9;82l=Cpyh0UCXK|ky4&nJ2K~Mq`D38g%Z~nUa z7V|6Ra)<&sVF<~15Zev6@^ch;GEIQ^2p!xFz7Z;r%-q@Mq7s+h;ygjvTe$hX|5S@A ze|IFx(+siS>kIE-+o<+aDeP#G*hu`$ zxBp1IQFh~X@VB;aS%fais+y_zE3z!HLH5s&Q8qHoI?FZ65Y^;xK&jz}--n;uK$pY6ru#|rrx;bu-Tr-D zTx|DgM4}{VAu(Pt*e%ZR#h!%iL`&^Npg7HuM#`^jr#B4R3FPzd1~PTKXKoC*9@kvC zKK56vE7Y}mpCV3;1Hs&Q82#ovg>Tp2Y3J*Mcdaj1uT3yul>9Tgf6X)hiRiCODIIn? zWVxns-E`pIG$kTgSkb1!jMHKwD|0B7YZD=!!ShFp|44lW{piE13t_&vp^k#pXXAD| zN0MO=X?VHCE`1r|T|o+TyzFup`Nr+|RwibNJ7oIR9CH6J55@Q(OWM>Y7YnO`N_?2S zuT_O{d3t2VcB4T2dqeLH2H#wL8BY0A47xR@O)^|>Ji>_abT5ahoCHjHYq6pfU9ftA+I<>#d}iWIHI?R+}dEi6AF zvH1n%*Dz+oLtV1BBo1a`LhF!gDDu%O5^u1yJIrqG`sy z^b%uk_NWMmrAqtmt`#U+ZPZW7_B-$Cl^qQQSn2(W$p|zRDoby&;Ky;~f8uYU6sdox zF5stm^ZW}}@H?FaSDf@s`5>`$&$VQl%XY?74N-_MTzPsvH6K z1ZP~5X8GnJq@sx|aQpu2G5^^6Z$6LR`_o&bP-0U};)=6zmx?~NJ6Bqm*uUYV z`a_6)UNq5plmo&4JyFJCVeOKUyxyiWc|iT9r-Q zlm8sF-17~3IGlE|S@E?3Ar^%tvkV%tBHO$1xF}&=l(Ld!1)E7e8l}OTfn|iq9RUDpjvkFF>rl1=DhqoNbuZw^Iat=U|1rOh}#N0 z<1)b%BWB(`UkQTe-8mhVZ7>1M;B>!S&$v#5f1a}9GGilC^ol{yW1J=*J zv99Nm6WaLVn~I6=l;fnY1@MRDo^17ThUiBOcX}^do@aZCgGW)pa$Y(85?|5|8u3p5 z;_?gD7QakAg4139W?R?xS*3NGdm_x1E$EoEdx7{)a(}{2@d9x|uJD)S4m-Oy1@NY) z__{oA2<%Ir~hbNu_nbC#A?+ z?h1bXo_W1W(0IXJPs9n~<$EQAZQ(}e3dl?$&Q$KAfnnkHSG5xTR+WoMGk)2w>vdIo z$%RM@?!KJCAXfWwE%4J0Z~1`Dvn`*y%5f4hf5csWi~9a#Z3xIIT`YRWw4_LW(eU-$ z`-AjEk?5E@7o7N2MzdJO*XPqrQLxbqsUsZSm<)$@tcNKz7u5Wh3}^YzbA|^re~dH) zZ4;Xe_8rG$?u$>H?S|?hutxagV`rmAA);qT^j$uexB#LYft5*2xi^8tTIv0_TO{u2 z$H({YRc6~wKdFd&g^_Ok$SQN1*f#wt+?S3zZu{6CgET3V1Vs^UQ=Iia8+m zUWtmnt1GccRBR6D7uYh&n%XuJ7uWzCvWP|Z*#QBmtx;>EexQC8JmdqQ@sUD8b7*_? z;=%5}{&GLw{TUs^9}tUX-jk9cXDUcM$;hb*VCW=phi?H1PtH}qk9p2KIE1SpvB-g9 z7f9H|s|=@--jDYlBKkzYbw9W#i^_XnB+%$RCyhR*ws&89vj#l8O*BcU*QM7PhdwDM zjY7aylYn*LzQ#F&ISZ|c_u7TZ?+ntY{|~UOgFed~8#0oBbIP0y#d7@&JK%f^i{R%W z%8{>6Qj$iWy5vzYRR9d-Cx|}cENS#gI%!lIY)Hd0u-Hi&eIRv?tIV}PP_^}vM!CaC zXR`)1Kzb@(C2$_VZ@)i5NWLYRRr7BfX*`bD1QM8tMG_AY@-xyqz*F#m2PTNH9Z7&9 z?7{h4>=dBuQ%V~3MFAyGE>!H_0l#mbtCVDtM#<*^wn40Yl`yeL9Hc?Q23{rc|6`y9 zVjvEZps4zJ!d^fRq39k+1lRjmL>~_bfy9N?Sl^R@$G~Rp0>E|*U=i$hh;q*;3^BiV zj2cYzfK80qj*$bmOCG0x4!|JXJk0WDmq8nZ>n2{sK7}-=idOV>AdOyk$un})LHl%6 zo*=YHAPB4=2$~=WjmtpK3&6RQjrhq%O92Qxy?miEe}x!ZnQjgI073Bk0^qD_R8IGD z08{IH#Iv)WY3}`0kOofIBnG#0juB$d0q2Yg72M7}z?mHm?3R5Z<(HpN9!g?>deaLP z-gkgK@3~4hXsnkY2$#+q+CEGG_=*EW)FFXe<=>;=Nho>&-pFsdZKKpeaBwJ?Bp}e! z#mXUf9k0ThN)qgwD|K=C%~l3_im#<7h^R7S#B-4=leZhyy%wK%ilx#n;RHQObxgua z6JKRVf5>TjEqTM)@nKvK@*t{Ujk8wWes`Z&MvMH-Pa*B;fQQ8Bu7HQ6e#7jjc*3HGm3|wbg?=ujQ?FxRyg$?_8nPY~xb({+?5HRlB{a)L` zP~hn2QwzkMj%WX620_pMgLSWv zUEx>2)BO&4d*5`K%*S7V`}thJAF{EF;K)|;n2lH0%Ec)=3eJLx)`{F(H+=NWvqhnF z5}q+EqP+@UEPIOG=J9^eB_82iXd}ls8*}V8<%q0)jK2H3F>j^#)1=twdZ@!Yr`ipFT9Si4r5?f*mfup3%B{d%+N6qx;NaId1iX)suwf# zA=O{8GZvNDx>Kr%yLzA}wVe4Sh54pTqs}ls`0eW2(33I!GBaF4`fXV1l>YA3vN_F$ zdv)13=62&|e0%5P`1^;7mfxwr3>%GqQW}s+W>vLy{aBL8k8Vpo5*I})&PRG2O6MD{ zE)gxep}|eMv6pzRT+STejq4CksO_~7_ILQL%KB^?g>D$n6iE21S$g@ZhDbLiO=0yO zebh{xWL)v_Dmj<+wCA2k9|V(@zC!nD*H+9XjwJ2M<58e|An^-Nta8YyL_h)$Rkm#J z9N(#C@VRt8ix%?&0_8$ck6GIu7cDH1njV;qcYLHZJF0^;o^;-6GACA{RTn#=rnQJ} zXW)$1R1|62BkT*}Yt%$G`}aS#SO3^r0`g`#$92O3j~6;dj*U!!O0C}-EeL$18t^Uq z?ok~Xk&);&dFKYpS5z7Qi(*v##!F2yzRaA%3e)#WV4Q9GuzxtU1zR$n3_O&#%sRq7F)Me7*n@xRJ`nlLkXj} z^mT-6_q)pvzFjWrYA=a2I^=n{u@g>;sf#l@e0d!nS4RG`d<&8BV>a^+Gv0Los0uyb zwv?a z2}>B{JYmG4jVspNcUW8r;$!d%a6i_aRtY+aBH@qQ$5dda#;2pEL8YXCHkPANW{HZ} z;3ew|`26BK9_>)V1fH36W8s79P9jOdnXT8j5gi1hWLRch7P9PVE_!zLMP^I(_ZJ+Ra`1 zqU-dN{Pv6(a2Ocv?{B)p`NO}2FCju~V^)EkQ$6z3U!q}YoG;t2y+l{2aY=$B=@Yv< zw`spr<_>4~(zrd>$M%us{TiJKZcfn zJx=mz9S{65Rl6Ff{mjU6|4jKB3K5y_)Bc<44@2KrJ9syhb>T8PtJ07Derv*8As)ETpn_95*uv3PdMC^_vxF(@-*J-aNIwda|Irp9%rcj~bXo02 zFj6Hp&nEvna+mtNY~4GdjAP?xG;F~}z{hT7=D|dw+jmT6L{Ol5#BucM(Y}sCqMW== z5O*Y8g>KlrU`A_X>C|!dnDw&ZIN7aUGT)5(3&WT5E2)WU4V5kp(3%OXY1>7r8k+Ow zX}i#+Wee^=yVxV=Afb_-SI2CZ+7%5S=u0Xnmhcc@`o;s7UZ3$OIg_r*TML*5>CVp2 z%h#0sj`uKp??t(1DF>6-Q|Z(b^{2~L5Y51q-+b_JXsD>u%OQSWlW((8BPGS_thL_M zW5Ib^okxS8;;^Ehx(`)!MaN8_fcK8h&lTt4RAc&tt@gtCL1c5R^NrSD5mIgxNV3kx zQ7T!&Vv>D1vx3G)QKrEfkC^T3^5&KEJVD~-m7u-OKDyuEEj^ufpEBQ*(X_ATm9~gu zFR|~D^}21JBi%k64u3b}XumR~UnGI4=}%`@EQy#fiYZYDvx|Ku?&|+Y6favY>3L+c zzCY6?)W^A{Pg%}d_o_<64v2arZ+F>s%nhBo!_{57 zR<%fX_1UD#pR8U8;|nPAQkNK%a<&=&rNq9A;Z`QgEzL&qi1k3>+q;{{*%Ew0ljDVO}jB0y7{VzDqaV*eSf$+O?-UoNJdvQ=XeYYm&+ zBgsxfnwUDV{l^}jHJQe4ng5oTe3bc$o~jhL+{H;4dnA&5v)wz8{=wa5>*2~x?~@D> zC9R87o%_D|8QApxN7LhnherJY(jyXpY9`I2vELV;Tb^r${mm+6$!)Rq;3+7p`1^#q z3~_q#tL~(@>C?qDbG63h2d$ZR+qW;K<5s-dp}G@;?Y4U@YE_vCtKy64wk0a-`L=KU z7t`OyqaT)y66S0?JFE@Pot+*8!^nQ3H$-SF(PMp0x2-2ziNlhi^@&*ggS)!dx);O$!1u4I`QSkWp1^Qu5iZF zs$)yOmxJ%&1n|;UHv&5r?D<8et%V)RemaVZgoi&`s*~wu`c)U@qZ-t)&~+Oh(xm$2 zK2cNteYox`B#?Jt;5s4&Y zm zTH^=PuJ$r)x6!Y`w3^KN81xh)-gF_~2o)Zjw8Vl&0@7cxI*8Ur1#q$kY?&rbZuBxk@R%YJM0|H>^fe z@|VhWu0bfq{G}RAc((3g1oLob$*)5F8%AKkYyDr078eCPg`b;mdqKyxtgB)Lc3)M- z2(;vTPu+_9u=w&ep)4LN$?06u%9&74S@k@jlf>?AUma*_l`?Skw%X$a3GQp{TLSZG z5`OMY!tW27bO$bjqk0)p-Nuh=koo#A7a=AdGQo@xGLeTTW5e_qNJn7-;<2`SYUM$T z@;0;3_>}_0#H~+BbQyFnAdics8g=rM3K3uQf=Rl+xnq$ROXj_3s7xg7IU#FIZS?*` zKH`g|GEhnhr5h+#1lI2VEg5CEibEnslI5$xS8ZT{rs_Ckbb;ZEHsZL}8J7qfdEpeO zN0_D6H;e9TDw!QBS@=inuXt$8s#tjas~1JKqo(CNrJ`+KM(6%>zlfV?PHdR`lyNdD zwydtWM7ihLo;Ak>UGX%S-n+4$JfiY4?0oZ*ASR*dM?UQsSLh$zAP1{rWuOPpKHrRj zR+7Hf-8eGBJSavmyH&^m4#jYiE^l*@WFD4~&d`FGadY{ExVoY7PmE-MEiyYEbpvIz zibCh9OLI7g%w*T;+01_+CfC4vKc{0&(fghgx$tBOgeTb8=bsQ%r`_ZqmqQ*P+bRnF zniq>dj%qdy92RZ0CJV?S^3hcu)4FDsG>N%5%{S99HtZ4t?(Q!zhq(D$d($fN?zHal+^l6zBaAw*+;tC=(M5jss0m|}+ z`vrB&WpT*nPF;Barq0*cmPIz(UdCVd{1S{`EmHrnDSRwq9Eo>dh^Sk55h>Lm*B;DX zW2(RhNkq^(Qd~J9YQJ`6Nih_ONL#D&+j{wNx6wMdQT$P-h>fY??z7~9*?d@I*vqqH z-tCuXS0!y@-)V7wzOtIt{jM%QYbi;aT{I_0e0XS7U?aHiZh6_hwllsw7Gl)>!QMw{ zk&2e~F5NW+_3-Q;6Y6T=7~RMFbyarv-xj{k-ZhI=_7n-Q`;hlWi4JZQD8$ZLhhurq z!AFIUEj;`tyv#OwJ&M#W_%2EaXFNI=BJrlr=M{yl-dR0^ZrxK~v!$35qhR_IQa1Su z*ltOK-Yz)~n139z?CEriCk6IUq$h5SWBj9Y7=0)AG1HweAB6od%+SNR(x6-63fpYr zeA@lfZ6DU+7=`9(n=<>r5a?Zj%PqV**36{9fB^1Zg5UItSzWex3&nCpJ6nXFttMlo z@WI?I*Vng>v&xY{+q@}38uOHfZ_9bUKP%dR?b}@Cd2J7N*30u#y5DMn z!Wk?fV;8a$d_XZfD6$wO;a6m^F%xLXYVXm$_Q%5%ww4d}0ytH5`ORzllQl^438+AR zXLcQXEfHQASlm@n&io^#vf#Z**XmleK^I9|`kM6mm~H4xcEs*NMI^>Rl9emlp>7GV zf)=X0nbG_rMLU3Ka=uU@d#{`SGX3ehk~GeX1rb=hR{Km>PpyQvEV)Xr1NzloPva9Xj1mTW0^VExJjK=s2>erI0VLIZGLMZm!fIXZ|cgVWo)|R2`tZ7uw43%uV89eRQ$XEUr=es0; zEtzZBfAg3yX~~Rd-BrG7@59eXETU?HjV+MeLsof5jhOYD#K#tV`se0YXKgMu!6p`V z*?&+gV-?G41fD6p$7Oi;Ew;?qRQTr2d?T|z%r{!oueUI%n3gNy)|{Z6yXUoZW*|Q7 zIij)N+9>HwF3=s6DwhN+eZ6w!8;?=Tes1%ty%^$zZP0QaZgES7edB~}vfkcMzUD2- zyRCW-QP^p^KS?sZbzJnqLH(i4+dalu2B)eK3GU(}vY8;BT-E(Qr>RF^$?>xEME#F9 zWMYp>(X#Sip52Mq*8Fpe8mxZRskTbOuw$>glq!)euTFS>hc@F@@?((Gx6?V z8S(CE)(0#;GVD{f)HnxVY0I%M45-pO`PQ>8QQ?9I z@!L-`A3Sud*eiF}JE>u3h5BChFuK!MTa=P$VAtpRGGV>6FV58ddc`?p$a0KwogjAk zUoT?izh)c?`nOJUx-z%AAntnghio_{PSM5Y=N2_apZz|Vexv$nx2Ev9McrN^-?|z~ zNqE44*!a;=60-P0QZ!Mcq%+wby0~S-xNzx0j#|lVtD(#ACX%7DY;pTQ&yXdVYIpG| zn_pT!lV6%>f#C2Y!M$G*@~Ac4A?ImnYZBwj>N(vs{RQ2LM`EV}kS7^g)Qp4E{Oi~4 zgYGtmPK2aAg@_=#`pvbB}LwKgboR>jS42PQCINbAgOohq5Kf2V1Y@+uc zFXsy$u@oHO!EBpPVTap!vSD~(uQMmxLkj%N2+@BX+C3n&hOTwICQYaK2)lQoX!fcu zS{LPE9Wh{lTgE}`Bv8Ou_jL?y`&thL^M{%=-w|r~pw0MQ%(tMoOU`oDYtWkRFB^1l z%G?ge25a08280T~zUDhvKu-PddOcKdLT2K$eV@Ebf_Z7*5tAe8S@nN?SAPiOo0-`L zyAvEIS%Yi}Strfc4j}nf5PA_DvZf_u-$;F)d2df^=7#3xvYsXQO1^38WUl_J>J!IyZyarXL;H>L^)Hqq^0tWgF( zniMqlUcyljhzPERsaE=m>+mwJCdi5wQ)#UWkB=H?2)T)=O^U`N&d1$Jww7z?t8ccS zN@Hj<)Zl|AHfv}uL8auwJ;sN6M+hwjR6ru~C-SdjBizjK0E5!}sQxxG}z%#5f-@@LC*BjHjHa7dRPPF3ARS#`YQVR7$M$mDz~GOah!0<?_WR4v|WcRL%WgqA=jFANz7HAcwK=Lp1*-2$Y80@s0PtYSD1n<&Uo$*gj zXEOR?*#U9nC|~OAvt|*hvzd<>VX&nt#!c=F9SKa9&sip1ggC6#iBx2&$npD)tI?x` zU~CXgsaB>}T~C*BdqJKMO!LZuZ?T`aGR7``5Y7?j))ZzPd|_ zVVp3Cl<9o{uX?gefo~pK_g_a(-2@*4?V%b%?;~u@f`i##%m;q_iI=dj|0lsBSosH& zVj9ii14u|^7Rv5*&e?xJyQZnY8?`RoeoBP-AZ5zD>L z1{>&G4u_8zEgzNFzp1*`uGqtQHuF{TdMEr;2$O4q`o)&!h0=tX9(qu=rKO{pLt#-o zR4A)RJ>v=OL%bid!))!yK?;r*%63znzl`BI*N1b^Q^HFE?!>^JWj{ytG8#%?Xq(Q# zUzUzc`jPwVpK<~Z@iUd5vtVp0JTnfSeUBj`Xs0W#(dk=w@E8GKy?qF+V6{l^7kudTbn9K1oaO&uZL{`kM z;v6CI$mn;A8Z*AR_I(ztf!Yf%^Wk=!lZHimW+c!qEO`kD%#1)(bLYk04VYT}9> zSQm9kgR$`#f0ZRagX@2f9AUoKjZ`+VMnPJ)0v-v*y-s*YB&^ax(Z#1 z{f^|rGr{v7u;BJAP>eVTYQAm_ig}`k>dg)>={kdR)a?-%=BQpQxSPh$7> z^v%$;bJj7VvQ=B5BFA2YNQ)koZI!4P!!2%Xz|x!GL2oz?_s(QI zt3u?`;Xzp(MkF%g6Q;P9o==y z8inV$;a01*=b#{lvECfRLZoVm*Imn3aKm!ZLr4W9lNyH#$Kg$M!|p8<=bO8S5Rw1n zh(@dEq4|4=i?R3uz*QVOOj8I7Z|L-H49wd*4qkP+Y>yQKkNbj0lXoHQe?hQ&Rx%Jw zt@Cf}p0NxqX2$VC4OP~E-iqBbPh!N@;IBpGn#_YnI8-$g6U@^^NwbCxWRCQ0=It1p z{glXKLnS^aU(Y*!(?~jTX#b8Zb~L~aD|2iS!69u?N>4@ILL$B2<4Gp2!{YgwE~NKg zBr&uPLhDDs9~JWw6C9oTs-^p0Io;_y&&D16n%-+CX_9 zv_TDB1{`W}^<)c&Z&pdqg59B`ck@%*L&H%tqowOmwx<`@r%-M`)}e`yBr&KZ?WVsP zHE%%s>`Hl_`JLAGAnA$3Fj&<; zKm5KUU(q*@XUl1xowS5&qUN2auzMw;gUB~z(XetFm7hq$IVKYpH3UY4w`$j<2d{%Z z=H8V|4)=Tg$fe?nr@UlEFy$8&#-8D9s2b%?D+DxY112Lvjv~(nprNh>Xifl zn%SlQnohC!nHjG2juq?Z;u&RSOxywvi>cLzx6)?P5kSEeeC1U0{>~56n>G3C=dfZO%__v=EQCl~10cLkOVAyaF>F)Ow#fSDfI~h9B#$nt;o}e3juAwIbU#w4{WU2MQUv=I_?sXL1tT2Xau6x9`I>~b3x!SV<$-&V zwhrH2pCZGS!jyj{gl|(Del=f-S6{yY`?BoLXdS5 z6}0Y0UUR6Xmpbum;Rhf}5*A;aP=Yg)|iB*}#Y{jvueKt}xmRp_%{ zo+K!HI{adZYqO&aWV1oRzZJ?Do^Q(pqxZN z?C2Dca|DBDN3hw!UI^qJ3~jL1(XSZ=lg$SBvv(FJACsVvSlHwRcu%8LsDM+GwT|FN zpzdb`%J8rsp!PSr`OnW=te0fL>XPrwn1wAIgQ+k{Z4l|v;-^uu_hS>fOYsh-;aQVp zTy>`imahEzjG2J!L3SE{9XDEq&IEDaQTbl9-od7c8V)O$F~{xw4eF6iHsZl#sr_+E zfKkf#A#3gf!)PxOItYTf+O~z3F;3b z#qj~}um2wOBtuRWX|TUPErXogzxK^Nciq*Z%mDfl_BcNbAUJq9L^>0sXIVuI6Q2op z4kCi6EflV+fd*PlF%Sg|>_ANq(jAnUsRkLabs@~(DML*Xn%x93FAloZ-G__#&|ADSB*AIfu6bqBMAqwmzhQXe-g09J?EC&`%MVufYHS|#P z8E^lwy(zG49onZ6BaKkq;{nMZT(*v053gwEnDSf4?UD4~!KghU;uYp#*zgsXgL5>4 za8p-2T@^U=(7t~#Tqg%M{5IC(KgDSSQ00r$V=mjq!#QZm^rcYqYS(oz!AipjTw^|P zqCSY67jJ=c9ENox|AHO4VbMjzS~!{@90?zsxdkE#hSM1u(0Nm*!w1J$H#?Lzaga+a z@)wJX1dp0581aylW?n*$B0gL6A)&p;E=Vsb3CMdHnD(so4&SM@4kg}P#ODVC2>U*& zXrX@spW3-E$!Bb>!^A!)?w?=Zb*c~sXxQBP5@^O)abV1tV8(+kagy!sbB@i5k&YCFYLa+MhBnZ*~YGz+RJrU z20U!NjAx~4c)g>4U9z!01F*13GWtT~(=QY{lc_J@0fuwzP`#T(kn2Gi@F45!|F|wQ zRk5Cj=IY6D*c?%NKbojCAe>;fS>$!r>=NCB959~$ zD{u&T_RNn|Dgb(XJ}l(F!tDxB*_VUL{^moJ@7syMKG`QRm;P+lmEvKUq9BOlSdUo- zF-(@?%7Mp3I`9Hua54cdPWuAnS}`JupRJsZm0 zvUnXj4-Tp25%V3n-V4%Q>1un?I5ZQOfzA+~41#q3%n$6{7$@-c{&VgY<;XM`SxSA! zyZY#w`*;{#Y<@=kyuP%KXWpn7mK;i>}?XS z5C?{gL9F2egKISyT;G6_{)ac-Pq45X>3axj z$@~6@C#d$o6m^vJX&)YT$d7MsxjKS~m+ePhZ31JfViBy?<^cd%x{hHN+Ze*Q{w5%e zLt*yux!_I|?U*G^5PZ!U!kVTi7`{R9#l{J#nkFQ)(!H6l@oBJz)cVPxoTk3HVTB&c zHzxnQ&HC<#e_9>{A3gmj9neaQWL{tIkf_p@FCidDzVt>@Taohm?D$+gElje26RN1? zsmKgixge&prHlutX^;mmasc^Ok^}fbJCV&|H!~z5_y6gcfG^+w>6ur444|ITyo+k5 zvB$&w8Q}`^4^sg@g;}?X6AzR9K6lovhuU+E-+&ESUP`P-)ZMU$JTv+CFx1!lPrVB| z?FNz=kT|4ghBC8(zG`Lna4aUJ7v1nb)Tv!P---+(q`K(y@8w}V_|~B!vtU$hGXGyx z^`K}{XHA+)lyV~k<)8+xb`7ACAmgu?1X&l5=1oNo-2_H;!D+sfuK7)${ zMs_1%`k*ikABYFlj71|kib)t$UP-a=i_1pODmT!d2V-EKPrxMUzwm4cl&0#O79#NK zFhT@W?t5RdVDn<$_$+vAH_|y9bnt~N|D^l3Ty@lNZ3GS@_HGT`E(v;;G+mT|=MOwA z`wYHgacQ&TnPfNeG5Cc-|KlnFDRvpN-I6ncXuT2$i0Oc2SDzug@%tFkhy=-gd7O|d zCJ5~DutJ$i*U-ne=jY^|sg!^Ld=@DAb>Ta+nd=?Tj1PvJlR!7~G!8%0cMftLVur2JzA;x~M2<+6 z0rsl1U{GeBnS_W1;>$gm1p>0c8g<;= z_ChyC!S52jE+RZ3GJW8&Xn~{ULO`xvzsSrJ)fH+=&nI!kcBYo~>8RI`rWd=P3~O~^@`Ee=#XQ@*H&;-_dcL? zccaW!G;#7c{dK_1*{8&}gKec`{5_rj(X@)oaN)y#E3@}0zwI#WSiT$Zs8_x@%9CO8 zdj)%n#EYA}+nc7UIgW{$4+}Q_+&1~hpg{g5w>($UDt11n`sh$?XfDvH`lUUei5su| ztD(6(c2|1=Lwb9*3JGneYH6vDZq;|U%h|uwm+B@U*zH%Qav;)v*Gj`wr&Z7C|49B( z4|W_e(S4&XfW4yk6zuyba+2d>H~D$%T0jgq_E{-sfFaX)zJ&IK+KYeR>XA_1 z(7x9cU=2z3^XGKf<%ir`KVyWvm2wUFzoie>7cpAk_c=FD2O_l#!LaQdudNtTLiy zoswOo3q^8VDOp+Bqf`oI6yl8Bm61&)tCP2R8s?pG=iI%1FMU71zuxYB=Z?qwdC%wL z`FuW}&-c)p8ls-6AhNAIO4qG9sIa-mI=ownru~GXvpbp@ueZu6y8RNdN6kn_@fc4X z(Z%C#*Lc(WHvQT*%R8fcw#rMk>>Q+CEx!;@{;>a)mi<|^(`t4?SALp5TiIpze!KGe z2lX#?3!Lnw=<`kQM~`39(?V&_h=w%kep`59Go}6F&vW*Rd)`Kc7sT&1e=C)^eCX_z zx0dT{UkXeP)Y!(0-@3W~?XF}plBhMone%T9QJVlpZ5v{3VB@Y*>d}RpU{JUwa7fGgs zg*ff@H3{6dNxs2zlrUe@tQQTR*-X*)=g+;D&=RM9v&NYo*B(?81er;>X+8y`uoKyG}_U6;#oEcE*71OF90EHWWOX_(rRyij`d zFg9`V4CNRq>|NQ74*I-=>JrmIGkIZwFAj@W;lW~XdvC&hLTy{0V-q_|#zIYOViw}U z&SQ~1+}O8v_G6(>??*4}mE5ml5KpEuBOA_74J-9wQ5vE-wx5n7qC=Kg%nvr2;apP0 z!uWjVf;a!m^o2s?)q@3j_qmeXcAFg(cjIt^R7K_b?O%nIhMTF67lI2Mh$ii zX&n!p4g+$8#8E0BhqCj3UNeGQXR!;R@SbqtV)q%wkCX}C?eVcs617^^^69qa{aVfh z&0A)kpPMtujPsHzL$aUuBa=~AX-_rk?{}?nvDlC`(f4g@q);okHEhS8wPyv7q2^zX zz`re)PqVb(O5d@=3;EBw)|wkGU-Pn-M_Ogw_jNg3jn|UYu6_>Bw{$uD%(fudAKzT8 zXMR0<1N>|rdJ`c+yA*lM)zg{}(X5NXzSTKD#jw@oMF#Jv(C1G{VH+-Y&@Z_sHm|il zF(G)z2++fyOD~2kU;UD{*H{VROGZPFW8)2k(ZYu#_rm@)YN257d*Egm^ z{^26MEFkK$ZO zNn3BHP>`KXTkkPSAhX$KIyB$kg3u~_SNy=ujg8(Xxf{Ly(N&tyg2Y(sfum2H2rm2L zdGlxcO_}Mc2ZFGamUvl z7XB!{$MQ4i0VDEk4a%uOmyhEOwr<0aq_e^6%3M%;k$ZWVRC`>i{K>ZEc@pk02<%*_ zV|@tm<{Heaoc>JDBUzWB@IA3q6h6lp*ORnu26uh(ElP3hgf~9R5vOs`ww~o&tk46N zMeZK0y!(5j*Y9{PEA0Y3FKt?TSbUoU%lnKqYepm@{TG`ONZG6E?0AYqZX0sx8WOO# zW+`gxyx!6QJU(ByJ3Eb&b#KTl-zw%#_&zkm_F<#9^ztn$`BdO|s=7w=(KOCSEnKGO zc2@_R8ldjv`J~~R8WwXT8(e)7yU}a$XT#>Uydn;!L4lPA0rjtFvK!}f1qv$Vr7VYW z_cX3!TBP0yev;?!b*iUJI;b_x0*;W3*D6ob^AZUg9UHylkgYO>u74b(`u$Qk&6>H< zoK3Z5lqKYWTBtn0H`&fb*&1AG?FAzbC@BBZUfaL-HlIt*0DCo-toe_J7coxJwG>?& z*VT`80%sLUxA4<-lR}7nG!>-_={WzZJeE|T_g1?L11tW!l5MwtjVskVlJGOqs7Oig znNsy0_etMpY(Z{3m+I^5-PLX$cxQX9_2a-9%@+~uI>@MRp2JLTm@)Bd>sm;ry=N)V zUwaxhurN!@^J|4Of2))4{kN>(oU-5@g$F2pyV ztln-jy3RLF21-|a8eJH&YXm1|1x1*zy z9ds3R?Bxb>Vs9}bXTZqLH^nM)z}wT&-yTH<-<8Puu|naltls-KxPxZR=|PVvr5|Vb z)1mIFn3;FOqV8Sn3lP`~7!{sM5yB6+sOe-{|8Mb6h!riHSA40!l95e5)DWTTr zro?0pif2i$bk>@gw}65}jAdUbbAQA%5WZ4aPMFMwn3t_P($gz?P7tcP5}0|uunK-v zWEMEXK194^ISAyl;Sc8 zUb^wdF2ZZ1UB*Mfr6&vPMjix(X5|VOX3spJqk@~%hg4E2e!|n}xx=0GbSJYp;LMKA zxdfFMWwz@mmy@=2U;CoLIlkc^eW~DFLa|+EE}+&nId}{aK8+a~s%e*Rf$^$@A!4R6NGTQUxwLJVO2bkwC2Zs|ZS8nG)W@jZ z{`$RO7(GD>Ie7^pKCmJ(%ilj^YPX2%*-cO9F^72-n&AoU^TZVTh>4~51fhv!Z-5S^W;!3OUNQU_r=Jqetxe8N40r+_n? zn^3GOy}D3OEYHKuRNHMqRG_?Ke$&X0!t)}r&9a$8{myXCS(pK*sDZ~Dew-=G=4H(r zZaB2On{?I`6-)*rT=>f=8mX9kCFI0ewV51Ce>mfO`V_vQ=ofGJ(7ls{K_vqa-3Y~MIk9H8RLqR{VL zLKhQk3B0-vJ$SCjewK}%{Le@>=ugtWfe_dRA??}(d+KJiGANY5zc0}TGsj{uZ&eO~ z8HrcmP);Mz?+gIm8;Mh-&K?Uwzw&n0E>Y&3jUY;eU^^q7y4CjDiR>W^p8u=C`xV}exg zD2uEb-&FmK=Pe#ye_VYNG?{zR2d+F!$Mk#MpxfQv5rs)OL`cTqteB4U0gv=ZEQz}B zIhl1cAIqQ!w#?;SPsN~-7d3nP=-9Z^#zqZ=ITL7cb);_hM~0M$C~cMSY>CNVXxz}7 zWnTqcFV&J!S9!H@^-t=^HklSaEKpE_Kz-(>%>I=X)pz{b z$$j#Oj&!-QRhPBg>NPF_OO-YfNO~>OeSZ~Y63gMF1iB{uqYTX}fvcZR3b<)O52|RH|`8_2DY;Tq3xtrk2T%If?Z} zi_pm07iz(Cbu#Hod-?+I_Odxe-8}j+CXy?n>0U3MEk&Qz8Z8@~AM zE}s&`z0>ID9=n6(ekdV54afZrUeK|mq=*Ow^PDmC%4O(MLY7!jk?aI{kpl$adiXn| zqwEcn|DphrZPZP?{Hl!ZtN85|o&5_d%4*i1ub&ll@|vPh+_&he=We20&zSsRdpWy4 ztkd+$#Km8B>p$!^Tr~8X?E{lP%cnsCUa6P23zVrS4?Xy%LeXvkTdTqNi5hlu?N`{& zQ?U2`HGmK~ZTk=PK!1@nl*HQ+u=sepJgMLx>Vo&J=5R`_rgb?ac)A&%)^ZOcsi?As z+L(x@Rp0n9&5+thOU4!tooL5D+IVM{*Ngu#sm~&kqqS?fK01XNB7{eiG;ky6R9p2V zb#75_3N$%BO`85VLwf0d6u6v=!F)=}T`^cvryk72sb+4+sv4f0LZkHHE!&an<`8(r zXsq~-uRV`2rvis*9_|oe?MiIUZrEt?*qwoq%-6&E?p`TnNF}fw={kNNBC(cDBPxQP zshFqWMa`OfJY}v$Xm_x`S{`O;$L{wbCCUW$fPsvShij-Vxy8BT;gj%JBkxG%UVyf4 z_a0}q<@?6>wLjT7x5KaLq11ecqeFU$1>u6Rn|aodO8n3+m1;TE50DfCZ|^&Y`xQN^ z+0*Rg)7)WOZcPt?@3pmGBViNvJ!=L^UIR)B39_-A^kG(1lp*};p zX}-dWf_#N*b%GU@pURL@Xq!xle>97`ZMHA(nuiZC*1zt1DUc!cG?|wETXc+6^tFS@ z-=~GRnK==_xC`nr##$V<+%=_2zVNT5lZ)*z=`Xo3Q>MkCH9tHti(=joU;^mjl#>Lb2|{)mA*G-{B_bezU@ zKB5iWr6ZS$eOZ$-FUh^VQ5zVmlG|SLh?8vj9tDv#3-#)|vE5ypr)M9=cYue@pAVIv zjCa@~oRgb4gA?a^b4I5Pc*&_=TJ>b<#!p0jGV!-GB;#^;@M9(}9eB9tCQndph7=@T z1$a-N67^7DM|P+(F|G1S`gE}+qGZa6ecyGbaVeAg>a*cC<`mk18j4EfJ+qFS)j|Y0 z9OrQ)iZK(zn}#)bEIP2+b;9my1p~Xv$toV8dnRW_8mcU{j}Nyeg>A~oj5Yx;x%=9j zhBc6ErxN>9Xa)DDYG4p8le!#s>PgB+{QPrSM5XbJO%dJhfFJ{bnR4;AuZnvSi^e@k zeP8Ro0})I*rcmxZmL&9PnpClq4^c4176ZSJy&ZV!eYGicmBCJJoXMVPQd_Z7k6fb? z(jot%MnV8f&>t}|>TR@M>NfAh4`yO!nHL^l_2-?)67sE^?XTGxie`P<_*A-|`ju#O}RT~$~;;HVC73oRg?DN08&hZr_|7C>AO-Pla_X})u= z+%P1Bg}?W#F1E;mg}14g*sDp#^=*>#bJT-?bi0O~BjgOBG#{&aV?uQUlUMO(1G7oa z`T1sTt=A#$<$|e(p%C|WnT(^>bcB^Y!R2{q2FHNZ?!I#u&^i7aXW58l?DDDweslaQ z#1P1)4)T|5Ha`6Kv;uOxL#1ei=pubU#;Q}-KKIF%;W!#Lkl&c4tC@OBsv>=nzWQFO z7ucaE&|>>F8K<$_!S|W`y_dEVtbeJ&JnJ~&Y&ZT0^BpK9w&73gYKUFxmU`%yyYG7e z3!hJBV#2Dh^*=H?@y22LSkllMnhEbNW=DASz_AUVKDf*0^Rjja{?*JG=NtlLinN2V z#X}jr_{a9Pw$%g$+Q1u;8K(Te5VXg|QsaG-EIh$>A^4Rtef5BK89k^f6U-&okwq7* zeZomc+rR)vb6ue_T}L$L;+ysjj99rikaO#%N&|7B&YXg&9MmT4s~g24IaV!VGvn}b z_>7>lFuyULEgt-vun)Y9Wl|0R(#ab3nGjoo=@~-5;j%Cbk5?ghF7F^%RT{K?- zzqi|8MeH-%zy!1vixt3wb1*uPg+E5wKnIa=jJ$7PbqiJ9MYEHM>BJR*ZsT@*(a1li ze*d`y$l-A7#ocyQst}A^NIV!V!r1>alWAn$d>miQd;j6FKrI-o`GUh2I7tKURgc{0lAp z=ROI-lli4m7XCU)70Ny#xznMThl&*J_bBhXBDUjKA8O&K|ImH_=O7eL3xVj{;=Z<6vju{i^#zi}iDBV}_>IhL;el)wBr zc)0#o)cg<&KUabJm_SGBRG1iDEyO3Nd<0aFMXe($u-V?vyXjtniVw8{JI;maEyLE3 zqW^IGN6}S0IG|GI^&tvzyyQYs|0YF$0>6j>g2<{-K=Fb$Slva_VeO<6wMFSc>3ZnHRn3yLp6aC{4k#C+s|Kfy451+78AwBDlt%g9l!eaN&?ix@k^qn!d zLmr7$Xx9d(^vSsTf-?F~gQRSikGyo`NW_rS{l^%4-io2q3U)Sf=^3%LwKCAaecf#- z>S8gFx?HvIcjKIk$&3^41)tCR@f<|n$h92u%mUW=dZ{XQH{Dry)-|SwV+YJZ7%6VDhtm`S#Ph{H1CscdpE(JKV7$%kTxQ{?LcIT*mr_i(T<{0##?JtTC5&V8N z4cMf=VBt%ldHYpiiwDv>@r{1^*uZNHXx?_vyt5xTwBe7L!|z1=8A4dhCl+4Ej9#rR z!a#m3(nZ2{!Xvb9`eO;)M{SNNoTUKGtMt%3&Uf32;mkI98QP-U&O~kv5W1;L?&(}4+q*PFvA$j%T+*EMJ9=JxYe`v}79Hd07Mb z5TTD%72bf5KieedP7v~94#+}=ln^dvBoov9h%z^FX%^@Ed4}{bYzPc=ZAZdYI~-je z?`0r7Y{))1JdE?*u|lB03Ub{@sxPa`B@?J$x=0<|k(`ZFRHP8K%3EVB2(>chX;x|$ zasv-zFZRZ4V62U<8NGOnS&FG}kW#R-mP?P1z1RWSfL8yV7&s0h)3VJJ#)8z(VBmog zp$Gc@i(?|N(n6W5QePpKu0x0NA{LWBmU>fTke%@Au-!T$($3(h5#NlP<@5hM_++6M zO{DJRJh1_egz!MTPpfQT;?*G`WISi#Pe5(b^aJhnYAiszL%gNkthcs>dvvZ5*#kB( zdiAm%sPbtB!j=rd*S=e_A58%2Ni(=k#idfo{feY;DFQQR3u_*4kO{icQzS?axzSZX zeG3E0j>g7C{MwBs`p@~Kn|UIZ5j8}G50PKdX# z1{XGP_1ro_h9#{hz0tt0dUwU4JY|q0p}X>YFTu1ooq^H3i^<Y`_-q`!wX7@x?_0U|vo6P;Z2{R?Fj|OY zt`X_5oG=K9${B(LVTHo91L&X7#z|sMZMyiK0&s-t-ok{7A924qs8>dCPN8d zl!>hmJOI$#HSFSlln@>0?6RqlqHGsN_&zEBjyTPYRb9wmCR(RG+nv`5@s8cN)b6}# z;Ugm?FIu%L+^lA?V~kW$X8}mz(D#!90D1~K@md(I7uYtPxcGec60uf#VC3GcJeCl0 z$Ctj^vDcIMaMlxwz(V98+!Y)JWzg5W7lm+JPoRxXURr^0d_~g!apt}#U@zO??u8kogVdoCel0sNZ|MA7SBNL>UPB!Fo{hl>q|Ghh?zYjV< z(1|j7r*kIo!$Y`pzHex0Y~m`G3nQao%!->EKlnyNd<4o}|D7L|vZo_S4j2BblAG2n zZeXg6ZuR?5;S4lX&GZj}95XGc3%P$Ap&u9*DVevT>m`TMtvL`i!F0}uuPR6vz9Qlug@9@ZH={7)TatkWH>}KE) z$1OfY#|}W-?uWLmybpps9WaY4h8d>i3 zS20m*-V1rQ!zdJY(&fsJD^z>OeFkPNPDiLPOVZX5p(AddEv|fBpx{i&DPazXI@-8~ z9T)fv`}YLhj?oLjZh6DRT)2nn_uxCYp*vjjIZmD zyShO2X~?h4eZm`N+koA^W`$X(W9v11AO1K-xIel<)3W)G2aNd&*2f4GvoR8N1&&bm<~74HEAGI&`ImKU8P~ zB+h|feGx>Hzc^LsN(8nlhw^yXzZ{mVV&T6z5wx3z17+$Dc5A$V*qA|vQpH*E=39ve zFJh5KrX$}V>v=f!=P;bx4xgSqST>|AaO2S}*)fwbfzL)v!Q-v5``Dc0?R)fhX9}IU zvUqGGeWGl^tfvab)`oWdUF-BHJ0M8BEfXa ztNz~hv2pd^A4`n!t$nur(xDjVcqgz z()&UF^}z=?LR34BO>@gO{Mj95Vdb};*2F!&9@|${g*o)+PwQOG=+luaI_3vY4%`(B z|0=NNV7k=MBmFB><~Zv6vAe0ccYg`I>|WE)o!x=! z3~phO?z09%M)uUGbNaWc%i=6zJ=JElQ{0sAxnAXC{&^9`&#_Ypt08lJaR2UsjL0W1 z>>oYkO>V3^ZR*PFIK&=Zp5gYP@OkD7HS&?>E}_f%EOMKzpo5_xd$`ipZl#gr3EdxS zPj}52`>;|p6QVQjMV;dH4Yz6Zj2L$+x!c+Ja;w6{DDlG+PifQ1s(OaqAtPZlZ;~r9 zx#{-dcftEWq4vmyWQ|iPd(OPg*wXlD`1yVxFnz^Ecim@Qx$Z<~e`?6NTw619e&k`y z?<|KjNsAXE#%`llvl9oahK{I^ty(Iyv}_bsJ?bA94W_rdIc5#%#XF!?$j4jwj9yFI z-cs<3KAhDx4(_)WpxOkkGk2IT$`@M3a6dS9xrI;2?C_I}X7(>@IiGTr+Won{Oq#TK zKD<2h@!bAFT>WwPJBhX4zvh7j->eZ|?kxXY-utlfhRIOr*G;0ad(N`WCm*_{k^JLl zt#kj9eT4&W>E1JP#(2vsBA)Il`dgcDOLyxI{!Nj4tsSD=G{e93p||H?m*lGTrl^o2=hl zyt*KCV(Vzw9YiO=?n#@CJEiaL#_h*>K2zrBP{0@U&?> zKs9fn+_VZSb+@wKs9Gp*AwNzQNcD(uIrd-)x+)!1@X7+4x1(J@oCg8FxjOv_JlhE_ zeKA;MyJE6s_u6ZsG(i!1LW1avTaOmFU2n8r?F%(my^ntBNEloUHH0hVhhjf?11b3{>%YLSXhp2JoI5qDtAO?N z-nGhrwkX}tbb=k9hy~tjX=RsyjQRRSK z4<)MPWfvB>hd%{hrI#AE?N{_>on9CuaI#trjkADO{m$j*H@=Hz^|M;BL%_jy`}+3i z)9qawiJs%1EwQF|=ShPffL5N!FLkQ_z4^4nZOdoru;rM^@RrP*xBu9<^t;%-9KPV| z((e;Z?|{}@_(Px^Jfn%j-f2>r#dZ0*Ve8s0&F2{Aa_@kD&Xy^mgwM>xkb8P*_HQ;4 zRlhQZ&)&N#THw2pSj*{@a6}f!359+*F^w~EE6j38H7ns2Hux32kr;CP+9xh~ze)RN zYn58%K4Rt9&RjiD+|CSzSnZKkRVBZa%6fB|Y1X_F(x_|>0tRX3G0@nV6W0wG7&VgVngITM}TB7_DuWFvdSlowL+P00Q2Br415#@lCKEAmE)A zQ8I~zL&_~w)My5c%-*g@6haQ>J=psDXL1*~qDU!~lvsJ6jll=Hf&82{{Jvdb50}(m z>y0>PJQX0^`s+FhGU3=%TQ1D_y4uIl`MJm++)X;{#8`X(FXNj!+@aeGy9qa8y;Su6 zJMcBF7f<^%1b+EMV0huE^WOkLhSVS&;-0xY0)BC=7~W3q05v=3q?UI3g!{bz41S&P zVX45%hliV^i1KE>C+YcLz9;zaEJGAt9Bxn8a4dOcR>C8kGxL#(B5&!};7$et%A;0n zEnQ+QCf+YL#}8`wd#bZH{uX zp@-%;wbV999ntt{HheXy0nYk_o^Autl@)Bs_E0kZHD(59l}aP?wHFhFkTPcu91d>; zv6iQl>PXxw&Bo+ax`O=5HeFQorB)hw%l(t53#Ehz@e3XhYirnh8HRH|Y4Nc4G7Qc8 z*J35b4;{o!CyU%;CJsM>`zoZMDSN@z3&S0_!(z(g8@4Cxgs{{*kTvG+V@TQW#4`Fu zYk-z2O!g`-uv&)$x^3v9J_JixhWK^d$52uW3X`o$XxIL~_o8bziF@fLH5lrF{nQC;tk%rwcQ z18*Y((awGK5A6uAXqI{CD^MP}UH)$rmJoJbBhoqSas&pn`9JmTrc32mVo5bQbSX&) z_g5~E|74ogdIa?y+_rAVs|Rhu?N=iS;pQzTD|x_2-F>iLXK#OIRl98;9O6!sq@X5B zy#Hk%eU7(Tt(}5M$=28T1W{z-GIMq=ybI7q;Zgx3y+01x2$$C<_}3(3d$r!0J`hPMJ>L0t5p~**Aq9)kt|=xUzv>$pau_8t zMe;E0(@A}JtHn9LXS)Z3v!Hc9`+#e#N}u*0-L0 z_ShHMbdlxa$+EY5hXf+i++A~5v|H+)9WW3nxa8vAFWho;@A5HOhK6k~NgGWbuL}t`Rrw>UQX`+p!{tmdz(L^NwHpd0YO>j7<5IFO;K~W6zh| z6gdPxr&4a{^jOK;*Sj>z-nIu=JG%t>KJyBTZ@N9bo-HiZc8}y)jt%P$2qw6WsU@moqwXkELKx9#Y;B^qKSNuO}|8X!B0JtEXDMecO&4cXBxta_q~IR&%t~Uw)=m>9#gb z+|TuFvuSllm&#kY`TYTuLungU(kQuho2iwmAcj!)&9UE}d73$>!o2)x zHhmWvU{%WLTUmk?I3ZV>7!=+6o-7onpJ+<+o8=n)Ntb_IpITB*3|CYFs1)L-#@g2!kQ zt~=}$3CDZs9y2e6U3#WUdmdPAn{eLunu1F>!5obH`2L+C6vv52h&)lJ;0WyHoN{UJ ze;zt3lGENC>(e|+RB-F5{wLNlQ@JW0+x+8Dd2xd_%SMHKxs!@J*dHaA1m#25>ppI1 zinUaqFHcpc;=Eubv7a5vhgKsGZ$Mq)$^U7!!ZLq>{k_Z~@LQgUTRBPuKQ8eNhm4wl zsy7mAZgu*gFyp9UeFPolq8feW*Q-Tb<0&*5s4W>Y^UPDR)GnS)1=00}?-ajvf#&SX zZ9Q-i(6@|#$>=c|QcN%Yw5BIP_@kqobY~W92EnOP#^x+G^0t!PZMz!bKeH?eBmz81wZ&_T>9TwecLkFe*c;z=uvw@0doLaYbQl&Or^4 zVK1!Srr|tg5OVcr_&n@2Kau_bTw~7=to;(!P*yPVy=NZ<2U=jk-0}-$Ze>d|eqxfH z5cUo(w{qozEus788B%;^4C5#FvQZ87b>v{&X`k?H-XCC*J-Sp94j%OzZR&~s*Asg6 z11egw9CC~-2&QtIQKmG<9;!P;fJ05r)H_&r7@4oEEywF!~|2XH^YAY3BF-5Oaw-Ks^&yW*Bs8lcmh2m z50@MD3S%o5;LwrIup8l)8pU`G^$4yFc*m! z(QBPQO#YNz$f$fA7QOwerr!`N>cHyw&Gr4`q_?-a7I2v`L|5(CK)4kBY2?i1cr0Ub z2^JR>+M!_1=G+JH{F4$wpiMIQc{f``yB2U)#;EFbTBG0#44%9;SLoGTYlzsR-L#-X zDNw@2Yr7D*YCne?lA_Bc7(vs(mMOC+VQ+6uki{6_C+5HHEHhgn3%)0osWC?oXMX&G41~BcFBDwf0+x|^cZMZ z`>uaF1Gi*ooF%BQ(qftXP2U+(kW!sxL^GsV#bc!LSPF~KO2eXMqxOH!8bd#j7}V<+ zVMPhc6I#012_eVq7E4V+yYSC}Qy2aWJsiqoaLRPAG)@UVEJ2uN--+k0f7i6NViKwr zskWW?EWP^O9#XI`M>o#G$mn%)SDdEM2mZ=LVQy7hVtx7AmtbSAvgxB#d4UWSTok5@ z>@c^srOqvIL}Nns#bCwxzIy+s;`%M{2NZjaf;A|AOy`Fpt~(#0h>HVh!)paYyI)}p zo*-QGd%(o;LtbTjY!=jGQZX}PmjMEcayo~bSwsfx^)JC)PG`adkUD-rTF{ptHDCdj z(K{N>vxt`hx`Pe7HgKidEMomj@;~Mzw(6&joYi%Y+UB7NPyONI!JqVDHWQ_HA(O23zN?n4&|N$JEV=;~vAZ&JnS)f4N`(68dvr_f?#yW|`*I@wX20R*9X zD^he7$itrOLOc@}1L0ON>%==YlmktxF_NI_B#USQn?x5O+8P1f_&8SU z9b|x%Q0F)JzxkY;{T5CJebKzn-6~6xKw`|?LJ0-jN^eO&P@XPUe(?OYSm9m}yzPAb zO?Zi3W`0PdgTt{ggV7Uqs$^jK5Pf6RumgXk1CM@1Yy^XjbKw5DZx?>;Uk7E%G_MO* z5obrk%rdgzCcuU`ReOoKcHh)A}CP0gOVoK?f=9gszMpdbCp$aQL6(#yKe{t!)UT!BK5}b zb|ibFWy##?Gm8jkr{&tiJi9>f-apKbyV8K>_IR6lHx?0r1|A0`M6Z6lfZz)AL-#2! zXwcm}A$D~GcU~Kr{zPyOaQ z$3p3sIin!zY!xWkSUS*9p{*9?HG<$#H^R>o?!arlWoe^#XtQVk@fNq9xD+E z{@WAaugg^92yW_OnX{tv3jxB4Hj2+gy)f&-{Z1V6-`CgchI`Wf_S71C0irG z)!Eai$BOPsvmHv=j(&=o*q0FFuH}qtrhibn;Tln@g)>_&PU5$Cy8V5ve28)Qxz(0qAI1`ux|_=m+Y7EZ z99w3cQ5bNM8%T=Pe&!Z|n&{cPD`&|0)OVwkYA7Mq0s+P@o8}L%GnH}8H-a9Rck8ou zX54v!dNJ+wRm%Te*=MQb-6hNBkDhbz$#GPFm(p5)Kb1(k_3l^VPLXWW;mj|Zw)P35 z5p@+lLY_6Ep10#8xNs(CU&7Z2mQc*~KDB;V_fy#w+N?a4xU=t6QfXz(LgLO;V`tsH zYv#K%n$lMe%q7w?Eh*;vc{rSikL9Lp=hX!#`aXJFBvK%Gok(#aaCA5EakQ1SEf9nBUQgGn_FUo_-XyDQDF+cu zx2dCnbg>E%tS@ix1$vef49=5ftS9SlSHL}b>zkG#RwBdn8R~BS!1-+rJ)GN@>4hfG zh$4NHUgGvM1I*P2Hp$*rN{A0JQ3#(`j5V##?#IN%%vKlS^d+HC*Q?pw*OanF`2@Uv-g8+~yy| z4sW5wW5OJ)`uP6Y51#_z#QdlCdIP}9g1hvYI6e%RSr<*2nC#LkR<0&StjLgJEy~kR{O;hHs!~JT7?ogW=yy!ihLlxkri_pba zK?_XOuer{GI9QA#n8Fy64trQ`LqOEmC#;(bmTYWbOeeD;xhSaGe(eBTSmH$+INq-? zL+b+e7bbzh$z$8{JP1Eo;>`+&Z6Pb$24U2vf~(D=QqpeK@F{C=w|%^+SZ-4{UpV2R zA_k;{J85m>XzPOYFQRmiH1QYu&%(_n)=$(Ybs9|qqW*}YeoSZu=tKR%+k5}K9O;tH zGH6j^;({pQLQoqErjqzAt?6K@Ccv+o4rccg;=Lp!S%=$o7(vyfFD#s=BHqMX@Iqosx7NvZW?9`((tHV zMeq`6MJ~ZG&jz(E;RibKW}(kViK}Pn-~&Y56e12YI+FFVZBlTV#ppIS--c9d{h!En z40jf!PEbL1j3rI=GZ6Bfd^LqWys44=*5R{(!wHm!N2Et-1@l_i=OP*z!jSk;-m7PLIqhn-z*@> zQ~Y+ALHsg5ZS5 z<82I3Ej~@MlUxOUaOuwvUicWgl|xZ|xX!p$bGL=Jve)f%!Q>szOe zoGp%J5xXq_!dUypI$8P8zFQCdv+vhi$s0H|k#)=y2zT)}um&fX@WTNBxeq6pB%0E& zmZ9cxzYiKBT-TsGdFUF|6q?-tWd;U0dJy9|zTsk+Y7jW-;D+6WHm9Z;^jm9a-j`!A z@zNnbq5fcY>sTVKKY_PE@Tlk7rNJh zjt_kxKrRDwNgOK3s+Q19+~07R8IYi!sjLn{ZUz<3slp7P12f~=S!nNY?u54o9${Df zjm_jDFGH>_loLy$_b4F>es}M<)IRP2_NU6|HU$Sk9vor9C*0XF+Kw%5YTjpm9{L9; zj}&<&;A`A6Kun>h*X?T-Ie(s_PI#dPd*T=RqCNB7R1wHVl3ouKM{nRxE_LlD1J#lx zP%0nmzc40pZC4W+DX=B{Rz~>3Z)0k~+ZX>FN}#glIv9WsLIJ9fNMwM87&Y8X#}3PBnk>j7*xl z0O7HlLLV@(@xWR}K=D}SsqY};dO9Y>?kZiRHWuau#qj(4{traEb5lKrcK=^5q2wU! zC3L~x{sz@!L#_a^FhnNi;Kh5zNk~f^pmY}~tb%?hfK`R^GM%yqcch%1rh#hwsLiS( z6-@WjpvbECcwHBM)k@9sDs+`Hy6plNcK_^3zbq4f47VT~V|1}fhEM^wf zz1WL_lp`V7TGwTvtKTh@A<)wQ8;+kM?D)&Xm{iO$C0L=U&c*auzGmV!M>^z90CMmZ zSjgJ`Dnkfb_v2SdLPA#1ee3CJLW=4UgDFJAL@eMH(8NRY{tnGM4VrgBSQ?g;v4fAp z5IK2_CM!*#*ymkYOf;Gv@#72F)6`JL4Nv=<+5c^l@Lf=I>IB!h58 zLVPDXmPFY>8@OE8h8Mi?y~l?TcIgp>(S&f}-Y<QzaY>(vlvsE<5?4KiU7Tg>R`KgAe8WQm^s;J%;;%;V>k zdKlq=5bA)o?Ki&?=v3NyYlcGB55@dg-0`=|vMK9Wb{q6>rJ3Lg>mEEW=#n{QdOn^Bw z5+T`YOX!DQZn-Of;HUZKpEHSwjb;()(3!|_S|pl@F9D`%Wb>ncX|LG#12ceOPe$*8 z?`yjboOiQ`A?XazlRbmJxusO`Whzg4djam$xS5Le9$UZLX}wQrObKIT@43syzAd_qzb1xM!Bk7H`S>C4UabU9=5AkBU_OI5DtO zG*&=2_tv(GqgMuuuNgp<<8?yJ7P>cqp-l-P9C(a3j?`J;Fd{i&V5 zmS0M|iPqb#HJGl?zBi^$Gs&+gf{gqqd3{&d6~-8kJ8D9n+t-*fMUObXP9m z*7grIy~3vs=YEYzQWD=l>r*f?kMtfmERetCODd|nzx%Fqo9a?|8~fb!N#Y(!efJb> z`FbyzNX{W!dVG`poH6k`T6}G$i1y8B;&VU0(sdvq?DBa3&1+Cc>D84?7c{goX;r)O zrOu|;)+15>i$vqxu%f7}@m`NR@n24O`Rv`kH}4yp$DI#p*L(#%V(&$#l^_o#%@`q9cR5)Xv@}otx`jJV3trMw%@`sajP$IxL&l4e)Tl zWtZhD=SzfzN!R6ThANU(-GxRu%B-R~qPB895Yd@B8QFHVk6wK3dreU0uN`N97Bx); z*I7+TU3It{c=;%2sg+IaQ*Qkjqe{Osu1621NCfQG_o%>E+&aa}?c}({6N|m=XUumt zcxCXaXHoWZw3>7FS*`m>BX7nn^^qIJQD>HG&MiL*;rUf-V#t5%N4$tqU(vJJgMMDy zJfD(*lqzkXS#?Qr@>WIPt{|o#q)sIrO*?tgpWX9=e8+OVT(WzpT#edKFXgtc zLjo!8`ne|qG^BUPaNOH*i?w&u)$7`ktB?1-M15~GkK`T@_|bFZ#C(aI$7RXqp#kQ< zj{M%4-1BAZ+zukjHqVVB?lkRu#rkQ%Z}zI(=fBv$1|HO#m=#W%v#8ARxnmUK70t|~ zK2&{TJFu^jVLMdL#4@ zq^y}HcY?wM^vswhcY@Z-ouHx9P$#IT&U5-2W*ipvmuz@&3i^_M>kkL9{!bQ7|7S8O?e*WF{tpu?o?F%bL1L#PS^vjY)BmAjo^Me9hl$;AQ}ut4 z*c2ufsSyjHVm03I`ae@x|HlpWf81onj}ff@qfz{zCyH&}u>Q|7xc9b2PzZgprv0D0 zU(x?@Q}=(Su>MajvHzo2_J6KR*r9t2a{E8mq>wBTrusi}+{n`A`ajnsiaFM2{h!Io z{*R5K|1-%{|0hJz|2b^Q_+Cp|NLr)b&pV1SKYgNKbWcM-bl5!tt?Rz}_lD_h>Fq4g zV8LrzBU8zBNU|0CF7o>KNwk_q0(sivtgVpsie{a+68cFJd3sKwk(5_gX;*1X8;Krq zkoJbA%@^o#Ad}Q_87)#`v`C2nijVrm!U9Ptg>K44`q#nG_UaR4xEqRXgz>Vni9idt z5h5+_!jaOgRkS!8Ezn{#&?0uS^qJgak|E`tHMH0(i=oB#8Axambepu*G>QWX3-182 zBm@ay7#uBw;ZGS1f6#vh2DI+jL5%>-`#_?ebs`M-dxFFO(k6zw_*Q;S#|4833*aRn zuYB~M{q-jq4gdu6tZ6SLBfK8e%Pyv*Z!vzHEKXQGSg^aI{eE1B5F64hr;H5@)!I z;z~zL)tRp_ zIxZFx-_k)YaFP`q0=^LO!zP_#^)vNPHvgYPwSL=5e%r<0{n`A#l~C<6iPbLKMEV|% z*V3TyNY+otMBkA@zOeR0eg>2Okb}PJA|n$$KUz#n5iK9*CC7=nSAX#}7yo3?pyQb! zkiF(2Qx37{(t+X(-NFgvh`q@XlPkbQ7OyY4R9^Ic{Xoi*{+-Y*7#}Tm3%b3A_Wfo7 zwC^opv?AZ66}eX;ROD6U^eJ0M2a=*mu0`b>toT-@#Wyec7}etdq{`fXWx19wfV%ZdPOe!EKTX` zLLEE$2JZFsXK9(c^4XDm?G0SuO-STUw}-k0_jyJ<{c}(FO;2dytju+Se+h$fbx){> z+IK9SlAfr?6*I6QoC=Uw0$$JIZWw!mJx?2;LcSTqDkS|Bsq^TkxC)=*5&aZr;X9Oy z3dOPVy36>@U!8M?j`rLW1}C-yb@b#f*6U08;$ z8T{^@@VmG4cW(P_pbYnz$WE%OPi*=t`bCy)LG=AeNsVxT2T-?hG$1Y9nLe=#kLa1k z(X+oszj)H*b+iCSI78tzM2WuvemLdnOBy+8J%5rrBH5W-R?^t+(MwLQcL`e7olE=$ zKnTzugsw_}{x8;3(F-#+MTMWz za|LO8RG+k}eaxjv4{cBhrP}KBWk+ym>gl6%=?51*V9BWm#aRWE8kY2y~=vI@&i~ zjJ0M=@$9X5@zXU@nDRfNg-Lkv)5tsmQUd555Cbi3L^}O4;bljD^a;8wu%CW)P;TH) zbEQ9x6aEw)34G{HuBSDXuRu4-&YsmO#+Hp;Wy?wq>l|>3fWrENgE)F+JLP{+JUz+B z*hewt9C(zR<1!LVXmOeq&?&lU?}ZxOcQj!|1U=X6aIQmZm}(k+`oOz%XsL){&qa|T z`AC0GxLnRL9jVeW5fmi+3G5GDp6*v4Pw8V@mK@N}!^b|tj{IhbOR^IUBKPqa`lh9K z-JQ$TIZ!T()zI7#x(r>Ccr9YO(xTAMxx^O|ogzv-7=@(zGpUpe-I>wSOPj)|RIUhD z^1Lcu|CmrJRg6Bjc+rmC^9$k_S!MFPCPbBp>H7*`_+%!)Fi$TE!`&2y?JoliVFpuw zTR>q6M~1>yA_}XI0TiCH6TaqkBMGjW!}{+tjB|AT-)IYm2OJB`tN1p1!hp-UHXdwj}{`aa5+GNcg4_iEfGalBjk<_C(2+3Ql(Rf(WTgU%$JgnJP=;4&U z1N^QMCiZZy!;!bEdpOC%72{#IMxpVre<5WD>*2hb293T;57NWwd%?_j*qq_o@vt$& zx#MBIhI7WlS`7dC@vt33e%A4@Wg{&d56kih4NX-XZgf1X@og06Fx=>P*q3{F{%06} zJnY7O6m)QyiSe+L-Lze&qK;@hY;F#LbH!d79S@5c#@2sjR*r|w$}Eo&_3H+`Cac#D zsxhLgL+CpgUdrQPeVCYAX61NT10+_7iS2r&7K^50`CiK7VeU*U-bUGnFin%n3PX`mlsxRT*i<}JiUhb|GI5Q}4o+Vg5 z9=6;@H6FGNPD$r&EpVvtV8wWt%W3E7OK3`g(&?F#}HzvUoh~{1&t0Vcy@g79g@AnxF5O3JaO7xoZK&4U*Rac;l>a z$Qp0WT?^27kh~Va5obk1)9qj4BDHG)tRl5* z0iH!@*8&*fOVfcr>v{mM!4|FuaJz;|{ly5Q>j6$AqPPtaM%M!@y@}$+M3`6)@GJpE zwu&&Z9^gYqEo2I%+slxtm{ovG?o^}e0c^k6Yu5v$KBM){$s+EnE+fot!q&Zb)RDq-S`RQeLLB%jkqS56$MJB}Rp~&{LlJK1 ze}cOnpe7x$!j$n32g)RVuxDSz{{D*f02^Z^G@NPAUk@-*4(aJ?Vm&~|Scx3-%K$lA z3{;TA^C>3B^!^sC2UxVu^m+iAK`TQAh73P_qM@gcG-BlvGb)8vuCCV7;{~~2?ur#U zlG2C_`r*E}fox%mIk+#R_Zgad3^dhmgWLi?oKy8MJf?Rr56ML6C|` z+R#bzMO5g>U7zb6Ouh^60DUuiI)S9umHEh8Ll4>-ijXs|H2_*e!aJcgZ0biY!cBb~ zk@RjC2mC(>SFtJyp4%Vps#+n6KJZ|Jg8vEuzYH7b9EKOGL1A6NPyf+y-3!e?sjmmH z;fwGYANON}uk?C!qSvE^0S&+QWY?o7x*n~?>yfz#T#qfs;d-3tZ0355=w~Ype)b>0 zhS0RvW8zr29`5bP^{|N}*W)JZH<1q~;H@g0Fnmto3*hvR)a~<>Cu*OsU(f^k^V{du zzN+@=`^3WbS${P2K6f93?_h;PEak3leQ;#8w8 z`95ABQMJ!feZQrBPJX9qpNslI`z(_tw9l-4)a~>AeyM$?^@a8s*GADkuRp@=vs52* z?XyN5)IQbML(x7P^ij3X-H$A6pMQ;n>+x$G^S^a6b3JbNmfPpWeyDvauEa6Q(y zCfB3OVP5;3)7xD8{EGQ!m)hui3WD#c41fN)P;YtuIk6VX>IPZm`18+(|0(95t!qgS z%R?cp0-k@a^{?aqXZ|@n8~E+%zM$lK%kap#}!_xk$z=Q=%qqWR|kdoqs-c9>s0y zWq$tI`6x;l+ROC(^Q#ux=i3fWX#TmWmGFG)^uqM~b677JS;+g^Ub%kf0eZhm3-9Wjc-mLL-Wwb(5DBu^Uu3pIV$I$kA^~F4^9;dd!-)Y z{Bw!Fq{qu&oAc+NZU2(ypVRE%$?{`Q#gpZ~cX7?H*2C=lbG;>&&Of(kr5ceiNcAyH zQ6E#QN#M-wr?2Bb)YdAt*>iY_Gp$_9tkrl=?`{iS5jwQJOPQRFCij43Cz{+DlF40> zA|}Ulw}lBSKm9dYEiYFUswK@nNBZuu!y((5X4BpnEt*;G3PqFL4$9$$0R1^M?jSr= z)S&fK7<6DDtAIxwgi_k(4s0DT_Ae-G;-3@SfZS#e%DPg;E>c) znySuDk{xOC7LuIYMSL2JT52>Hc(B-E84o^daE#6;<)$=}`f1-Ncvxn(=?so!}U7K!3>-oEQ6nC?$`Fiek z5G8EvYPy~;4AR!~aw+z5JsiOfF>Uw^V z^gLoRJvUX)eVF{l=gNAXcvGt9hnk4>eEdyKJ>U6ES6Ajre7lv$M#CEF> z^b-29r&6ot;#27sIGI!{RWD(0C%MUeu`8BA{NQ0yzZ{w1BRf3brS^@mV zga5k2e_dIj>SQer#QHi53moi(1rFqEX-jHW`}E|@e){(5P|+`RhL5ZmZmofaTSq3X zWW%jq+tHwB=T5NNC0(pL$D!_=C-;l<)E=zvOgx0*D|M3BreuhLo-8o(6rTS>fp%h` zjs@1if%iJffzD#!GZ^Ev4G2)IU`Z#pf+klY7&U(;vfk9S)CXqx#KJn1@$un>4K~}}XVllK~H?I~ICqlyI64#FeK7%`B zT^Q>>H=@PwDlL8mcR=y$Mm}nNeG08xbRFyyR{GkDyMm>6fYSHpBcb&52(h(FqMce= z+S+b_(ngB0Q>zXzJW;!mVtB&iA6(k>H1`#>K^U3Pzc#EdBRvQy>d+ctJi=d}F5JC) zLTJ%!=#X98sL@GnRDWn9kze5G`eGS#8J^=`MhNFe2Y8@y%X)#cE zCN|*|o>Hjrc!ogX@qLn^tIQg+y66PJFpkr&{-o%f+D2bq;KyEGAk6oCYR4Xp^ntl) z_1-;0UL3<&4}->!zG;52Woa~L*C&~tCuKcnez0phd4A9xXN`rd(^lO1!E){7`N5~9 zP}V%iq7%BD`N4N>)$@aeN=ic>n<0}f*OTT4{VxBv^Mh7SFlKsWCtUQIf#O9!1&6+9 zr5XeAZ>xA*uh|~W4_<+kOYGVQ^?_?YqanHW30q9h558%mogci{Mms;4(8l8V!HTVZ z*7?E0Ar{UL()&<;|6d!U^Mh++QCxHzqw|9!G0^^KV`6?V0i&{d8x!+`jt#WW+u5HW zROLt!Q1xrF(fPqg_c`-}7cRh*UfwoPJ$L%TqxP^1>M6aZtq5!KC(HAL^Y3%!2cwu+ zLTeCALOeg!%cS+hF6^G*QYGoMOoH#BX@o<-+k5mV9C}%vzR*q%@(v! z%nwdlDLu{KtjC`p>>-D&PjfInKj^hmnjc*50?!pyTPdFU3!TGg3~OP*{NOOX$@#%= z<3;gbQ4R0(6jns?c~PZ>%!|}wC@TW8*714qrMb+D7@Rc;vNrH}ajLnR7cYL1c(DvJ zH(_4nIs4!8VtO1n680YeUUY6K^5Os-daIfjd7CSE;n)iC;v}SOW4v(h0Zli_hwx(Y zI@7#37OdsP_Fye976)6*3+tvoD=$)7SjdYLf8q5X!A5xzxevv)2{y`$#{2R5$6yn@ zSiBKM+69~7MQ3l$_)IKXw0Pej@FLGsqrBLClf#SUr=i3rHG>lG=nWm~7GWxRqHmlI`P zsQ%&%~d9l2n#1VL5+f>1e7bh_Ro*(B-3+qF((E3ZCH1=-%S-C}Uq0)YrmU!y#(|&H4x}9WSZ(Z_HUzUm(9kpc#-%zc<)Qo)T#8328|^ zERjbq&kGWk)JtR%f8i&;c@L@>$-TfZM6GhC*dOsi`vu{ErB_P;OJ}^LzAK!MUX7)( zfdZD+1C|B`p@sJQA!RbdQgkP{LlSGz{jXLi7TR~B3+;PSE{VJ;6DV?tIXeV2=>p{` z-hEcm6z`V+X~JMQds*Mv3|&$HTWc@8fRDS}UOOJAe?}VXXf_&Gy^%N?m+FTW-rr*s z^F2(5;x5oX-2;&AqMVBQlEryqEPd6+l(H%DZ!&!J<90u*nbpeQ=6D84tm{qGNK3C< zgZ%$JVL(UPi=E)oJwQK{n$k`MM2|R}>;`|+3xBhRKj}vthG~Q%P1rgHaKO=_W*`0J z-S)NUq0VO9CzZx(D;n-U5CFq;huJ`g^SCS$#CGgJVY6A-DRH*JATeyxP88OMg{sBquKy+!Tp&Om!@DO2eqv1Q^cM7IfhVE=v3V7y0^^#sQps^96Se(WXzCQ( zPUI-gfk)`)r^lG$RQ?JKUsBw5y)5H)eItz9SAOIS97#b)9>%*3#HNpL2S0t_y#wNq zW+6j`h2qRhF4CTbcHj{6g?y^y-}E0zw1JV@ZlOr(3K_bYLg244Hc3#2tV5s<4XsW| z9ZLByXfcr5hK{G}TTw{+URFW6c_V=IqYDD0k2M5HUl1U@d$#=kTcVphFi->OnR3W$ z1A#Pow4gUUau&>|VgHlE3fK^(k|gZ9(pb<(Fcx^vmT*4fDKvv>{tBEIK8%|{e>ml2 zcPbc2eFcFR{PovJ{i{H0KU-E6_>Q7IFqM+M{|H9*!)y$3v}z2o`}7XgvRVa^A5ZdA zjr^1&KgG#UA@UPVezudJYvku6`6*mItCb)5QP*E@-fF23`rMUBpW)dIs^PJlv_2?% zObK$m>re4+p9C|Zigi+H4diuFJ8@1OmeUd3n#xsDM;j50L~uMj3sqWnkL{k z_aWnJq$;S^NR>cqq}&ZHJYi11@p@qDAP>gWtTBDrA-X1)M?|gAK(RQgh`;nf3dIa% z3EfCL_Gkm`_?N1Lr@I%(OQXVRPlbGS0x}=?ipUIME2Ri8&-of#5~b?T1qs}PuG>>I zJsx1q3J2*&Re`!ui1pQK!oRmAjch2J5~o7~&@&s#ro`Fb*_1e?HS3wR@MBL6Owk@{ zP^shqawmSAg*rrOzEW50o#giN&mf(8ogT-X^HofShc_iLq|4_#)sBI}vx>IwuwKtAE5qwrU!cwd-z9&k=Mt-bP`IG;@Fab@W4IRCAl)_zov6vL-a zte^};xFlyG6wd^9YN5ZI4jH@(pi|+d)R#w9EWQw@{yDp9E$kzLuGd#?pz3(Rr_kH05(lQ@!n0)pY z?)WqEkdJ)R%R@fyhW0h#zHhl2?)yi@sihLE%TClUC815rx)sVi4Nz1UM3yRobkeowSQKB z^C2J8~G#N7X{t&v^DN; z-dk3SyUNGV)Y6_S0`BfVHtKIq^aLjfAiBi zpgQY>z2t8`c|ukUW{OobijGwA)nfqh+$M16IhUazE)Ml>y5;`n!zWZ|J`bm)>vT*& zvsW#Jzq!Jn5}N0f=KGtSPYQiwW!;(t>&klwE$HT1u#{u=cl;6L?{o_ZwLD>DX zcZY@UpUC_0{hwS8-+zfRCcpptHDvG4KeflDAVatf;Z7ZK$L34p>wTDj-sbXp#N@6 zzq^lf-wUsuA73)u`3%Q6G!u^Dj*byPkCC#GcZ`eG=?!S)2vpzj8^yP6>py_)Mc(o7&9+s5PY8n;Txq)|9vq8i76_U?yW*W$!VPE1nB;T6K z2Xp1Cnvo~q25c0;c=bmbF=nZ?1 zKFwbc%d2e;+RO2%XCGl__;TcZMlz|iePuI7E~3_tsx1XIAEK5#Sa2OIt7;2!^g0T^ zuSbuTI~&#Z&I8K|{Gw}+g9J<<_@%=|0mbrAzZU&#w2%FMYY=KlPeD&dry~N%@iH2c z9)B1Nr@$0N(o`3i z6nL0y@(hwVx~=tTP7$Y@9SE^0ZA zu1=rX*h>w`)0V!TefntncuoH267Jwv%Ql0EMPwzNkPKZm@(m_Cz#n&1lfI?A5i&HxL4S4h?uf!vn|v8}8yCRi+qNIubH;d!f7W zJlq9$B3HscD&ILcmHb=a_~*{!c6}j-qQTDN*V8}Jg#P50Nekf@vUCVl(c8YQWXoEK z?!aV6v6Wmu#9E186~zbhr{}v$_+Z272lM?x9}aZhQS=);Pg_GzLssDv(u~|RW3`a+ zC(XzSCsn_gz2C_=@~vc$FZB{V6Y|thBfLHNTR1tP@a8bP@H#`6y^`V#PLfXefi;5O z9BB8S=^1$22k7_Fs}?vuBcdY-Tq*|o9GXYNmea##WG+_uaD9Dhef>KcR~^nHuvU06 z7w0J_>GNML+Xm!ZB684Q4bpj)lj?*{*lmmqrq-8c(V|k zn+Cg*;PA3^9Caq#G=`hS<@@DHT@t=U2rojzAAS(;ms++P$k~(U$Xd2R<;fp92zQWp zt2``6KY(ze6cG!l*jD&7)v46{51L!06bTzBhj~1t-%?6a_($Px$*8YSudmPWo6=D# z!pXMuA*=$;_9fq_u7WMpd>4NYa*`4G18Ti=o{}Ro?f<|%R3??SH+tWcgkSw1Z*Kw} zRk8e!Pm&o(0$~C;7&Zx;?1~~9jA#Z4Ok@JcCW^?SK#(m!B9TpyL>VtpL=aR|zzr1@ z6%+|1VNY1Y9(GXFiNn5xu+G0e)qS_g_`dI*^FP0L9%k;n-PP6I)z!6ibt!47U6hem zV$|2#auTZf-4#So7?E~sM1~m`#@F|W$@B_}^p0JNB_-;uw^=m$e?X`IY#WsG3fOu) zsWC233dr6w1!Ui)wMzE(%(gaRz?7t)zzyvu=IL$Um}keHdBtimneytGkU8Kzy+%jo z8>@y)V*@hj)*#5Nm=9!f@7IxWT209O+)8kf1a$fPMiQMnVPsFqO@jISt zC5E2rF)SAidDIubKr(25;D<=g&TDh|EBb2r9e3^%jK$i<@2do(^-8|ks|+VsGHZug zOk;a}HNFWG(HUmtMOetcuZE~1hI~_qX%5dr5zu$*lN@&+_1k-dM0`{vR&yg7y+CQ> z-IkCxl9c^^$0w~IZSc=0?^BgIdnU}XNn-YRSl?`+?EQWr1%{p{0OZRfoG&$J{PHUU^Kf`XiT_i>eo|^4A09 zzRtndz2fUC!`J2fbtb;9q_6Thi=KT$sAzV%qEk@xEdgjuxv!J(b)@+EwBhRy{BT}F)rilP6$Fc}e$?vF6FH7d!7V{sk?QOkuVDlK*h-TWhj@@!3 zimv3M$AHeum$`|oNwB=^fM}cHZCy`n)hPcM%KyUU5y@Qg=w*GQwlw4YIo|eF6Z+Ac z>AhnUQ**Z;rRu+O^-@&7c}Z-}E@c4=TvlT_!dKC? zWxZH}X|OpTNl&I#HKD0#cdqA(-)+xs79w|Nm-0{%_9Op#hT?0f|~ zUX?*i<03m<98a#4nl@=`6JE%5Cq^>1w131nSB0`cbf>r8Lcun0hAIt0ZH-qW8Im8X zDB@MfwaxhSUSGgkbuaVDS7$Mw{Jojrlbr(jjln>1XX`I!9Z{Or{7M z`pCL+2YpjzT?%#3o>h29GOQ=LQF>P*#M^5|u66Idm_>HzM~!<2xKm3Z_o`EC&k_;e zZSNsfJ!cKiTc6W3y>%`!Jyyi)Rs6c)X%swP5`KxloQa>LuSTn@!A37j9%Z^{>xt8dsk(&p7PmplG^wZXJ9MohD7+RG ze=bhzLtk0E-$k<$c{d*;aIBiijt_5i9esV&LG86%doV_z)&(FLHScW{JxKd~Bhz{~ zOR9HHE*Bqx;=c2Kf3mW%%^u!?nl#nX+Mt&lCDGb=!Hm{4K5-CL{|&T7Tjsz2-j>m1-`VQ zjguDG4S5=V=Xa%ecOSnaqT9*u&Z4ru@BY?M*q%}#nb?i@A2d{rQUM>{lpneWei+Rk z#6=8&-+IVOtph)_lOHkyKg7xpeFHyK#D}aSc&8c5FXIS@;o4;_U@5r4w&dG0e!t<_ ztsG@@novV+ZDXZxQiQiAw(%!<`VrUMn(P@-l$70b8eF%0$Yo%na-I*nIF(}jP4&L} zI~xGE&R_%J42K|!s)!4$9%=FRn0MXpRCbvxln*yhRd~-b7FFEB0=|NYL)i<19U^w2 z`{ncB6(Yj9@o8@BuZi5&-LkD3E~RZ4DH}O&5hbmybczV+w{VLwWyq%bOuC~#)}-IP zi>36tx`yM^|UZApbrqd7N_H{ke4bt!P4OHp3!N;2P`{rVne*1o8kbZ0X9Tu@Z z(rihJ6n}qNuZq8nbGL^G=UKqH25`0kX9Nhll;m)~sNl z(J}RMRb?=gYp$$PM$FSjMXU)B`?Z2eM(+~KBmWXME^I&~_o|1FP}z;j*{J*wDu-5- za{o^6jRkV7ME4tN1>2(FAy#OyvF}12(=^y;r*VV|2Hfc{f}zb{-!y_JA5=j+0uWWq zh%Twemb>xwctDfdbqY^^FiClV2ees|GBuhv)G#-7++R^0&{158&s6n;YoGf6p$k@mnTCY!u*4{r{ijp1NnzoQxr zr&5rWpHXSX^$@4B#XdKm9t&SZ8|O$(&UpuI^h6tWUF_0WsIZz?_*#otBo`>FVz zk)n7*z4);A5Wh+ls)-6Is4&oq2=5a{c3+XYoOOiorr`kU`DsImq!y2chV@5112x@g zP5qq>P#FNV!YbvH?cQbY6E#UXDAr27ic)V|g+*UPQ537=JclG2zhh*5Z9hfTk5TnK zRGp8iJ-Mpm?kOO@4UB7T;F|wxc+i=9(=l*90ht0rl>kx< z(OhEuB`~hV7~8(6Z6jjZ#q6<#TNx7rX`q3Na~zoX*pTA>oqIR2uB z?ODh$q^#?h?F@CTI~nH6o-3lvS`;CZP6jv-?PC1OBNFm?UtpUBGamceUkM+^tR#yQ znFNR<&1=i(qGS=Whx|{qg&D`56(s`1O8geKqkqr%Ok-iVbF@?G9<8jOaWcC--lUII z*0~8c4!$BhJ7zx{>IRO>M3NTzyMWi-E}*%HeoxdFOr8NvRowSu3e{f1E+M9i>~ahp?=50x;81|=_FH#3HZPw`dZQLNr|DZaa_*G z3o&e_d;UT!J2r44h7ZoRU!@ILWdUR#0Ric_3ACIYOD&s`VPCAtjPURXB(K;^i;u|N z@A#+|Ti`s%fUx$^DT_sKg~b(Ky+o0FTbfPUp8*;~N<$0p)GRMjPO2${w1I&_+Tfc1 zA|xi&3?hU+Bul3TmabhB3VQDI4ze+tI)zm7lO#EOJcA+ZGrKKk5skU?tP{!EwJeuPqKh%7ArZv4d&!% z#JGtF>WLzP`hv*dqef6a$RxZ4W826Wy%sA%gJNZ9*mE34od7%d^9W0$UN)^Qthb`F z8Z4bebACLP^;;1VRv4qjp$R|GYM{@V!+sfQD7QF9jYFfJyVWdU=PevMHaVhv=vdin z5$rOK#il;#dl@>mm4>6FSBUVNCY1ajVzWMv;V7!)#pc+oDU0<;s&)|^N%hY=fg#QL zHEfpEn@_3-Op*JGBsPC&8g(_QivWCOQMhnl6VRpxpr3~`(0S#6jsVbuC*?ggJkUJG zJlI_jcJ&(|$8Sw*>vz0e1B-?@q?%+35x;`X(fHQn9nIS22=2HNA+|6%KN$^mVWuRd z3NEEzQ(r~e0#4n{+6r>)+=;Qt96Ngm6JLGy=y|&jQ_r`w@;i>mA`MpWJ;||Tk2d2F zm5(YSmTU&hmHERFLg1^^kpoq|PBtMu53LgH7s0+SluLLj@Bc&K00;eg`Vzg6EAB=GPNA=xNqlqPUHyUuNri%llG( z7@TF4AL{nZlJdjhG$B8<-<^g^2Wh7u;{V(s_FTpG@@!pid7ro2m@LC~OY(#3ZcTnT z3i)A|_s&Y`Wj(@3&nHbjFO8A@8c1KLsNbcRq;wR}x(5Prx@>`TV&0@Q)%3a_! zx7aWKuuGZ5S?7qjMsUnDg5!^|KUFgRig$r0ZsrK~9eh3+Xz?+Yv>&8H#>~0*S2uYE zHdjY`S7^a&=&S}hy(@@VEj9kPI*)e3i;=utii4i7s|RJ!^N;VPo~JW_EdWqjLFy|z zyid(iJ<(ipyk4>ZCAVt|3!(zf4hyQVbafbpO;p~B%HP>ccwXbse_KH{^y;XKRTY=2 zs(^pESXFVERF(DBG*#sZD1PA(q5k|-3WjD?<^5{B8EQxz0utwK7UP(Xt7cQ9qIWFgt_5sg3w0}BPOA8J*D54X7$FXm!fG}M?c?$QVg)ebbIYLmEHEUMiv^q6j` zk-|Jcc=W8wT)&btuK`M&6^cx&SsEj{F&)(j@wyf?lH%6ao`IXAVlAbW8Dc)}(g{rz@z z^q)Y5kWe#4l~#dTWA}*7hxkTSZCym?P2q-;Q13DBw4=#D9r7d%au<~ND#Q$VVHIY9 ziEnB1JZ^!U=j(5n=J}sDEUofy`o{||8T5*z%Kxf=JUq}) zu72hJpnt5n9Z^pI$ap7Q*FSD9miot_6T$S4MwMAj*fH0vf83s{s|v5)#vyjc%QFI1 zg@i~_75bkDu77N+#DLT0>iWl01JKjA7^tHhP+tTA9ev_I=pXGWYx+l($p4^!Bt`vi z`p2M3n*K4hvZ{X!KcVX%Mb8=ZkA9U^{o~~mtbeSo!}`aOn-QdcjDY@;(BGtgEdFow z5BOJl>Hd}d6$1P#r{P~o@=Q7@d@RL?rS49;2t4Ac>{@vV{uWLzhVajpg{(mgd6_kc zdu~}Z{p8a12#fdZB%z-eoh-YC@L;#geU2(2rx9EWspE1CWuV71$1D{tNV(H3O8S!nwg^WF?w zd6)ATn`*0TUlt*4b=9A}?l1Y;?9AzGv&onU+HG%g5gF5N+Pz8IYNHG2+n5p%-pi_3cf|e2(vwob^@|?kd@-}8JK}0xo zpU$=~gc__|q8qM7!PzLd1d*qDzb9C2s+N(Y+(BFk_h+EhCYG4r(N-swiPY{;v^xyo zb^%<0h;KhR9AZb0iotsgeD6cO7g6sk)SFH9(v}c$Jv>MTZMSbZNDbPaOh~-J<5(O< z(p84&+vFE9E?amSrdfF-0pR^LRD^Z|@D>hyit6^7#=(|lNDlVxG!q9~I8Ab}>VqW* ziyUm=VC}6c2Mc>m<6r|bn1cCKRJy@kLnGYJ zW|IeD#V|*%J&Hmzg{LNKs@9X2-klK|C{X z!w7TX3h9~I4mAHu&y4KCb##G`BLs9|ucdq!@+?{xL@PT=<^a1AS9Wr!-%%7MND6^5 z=@r+8KxB#s+z6MaZ;TX1MtWl*r^1Vq=8ciZQ~J`pdtd!T8`_7~R;J2KyF|w+)yp&A zhk%}fm=c_+FLB=x$6-^{xxUL{^7YubxZ~lj&}hVJ{>j3t`F^pCtDfMdZdVg`x9~1- z#-4TYI>HB$|I=@^l>C7wosBK0cgfIKI^z7(8tFxmLXn!b?|<~7JWnv8t{A*1Rom)b zl>evti#O!o^p{VbG3X^8Liu0xmsffk$_pGtK%-`z1z5B55WcnG=eOvV#6;urdOsh?yOIeTenXRF@ z#1ZYcDQ$C>^qRyrXT7hj5$9_Avm!C}yj450ns}C!m4q?nj;uC@LPMR415@O6#!%&% zPG~A#si}Z*dGA}x4GbH^4MfWZVA;GyI(yzzY%nT5N|P5`$)u{RC8fsim@g^0uUj{X zZa~Fku6PC&|Bht3d~AZYk&c}tH`0?cOop)CGvs-Rf&IjJiT2O*GZ?~}bJt)9yYPy( zk+NRosr&v6gCVSz-?8(yRi2kPBJ5yU3pG1fC1>6_AogQ~P-Nng6g%-z5R0(tgUJVBNb-6W(7NLP+`8H!fbWo96{Y?3J=SE?Bvznmnw{zfsdye zMw`=%Xznbu7sA62Ji;SP_N@-6pg4TbLLl2rETPR2zvJu;tF%UjV*Tz9H`&7NTqSB% zdZeXIshoxPhO6;2lTEWweX^W|!_UfD*!`^0|8+y1g_qN{S?D~RXQ6nKc@|u^0%pPg zp}`v2m1(3h&}|W}`n$fnrA>ocq6$hxa7dr+p6ma^T-3a7oQn-_80O-Qxk2aR?=tgT z94u4kLPe%7keOW3G$B8h@q|3u%Qzv=Usoq27F9Y{G)>4Bc>54s<`Pz!MVf zchtFQm=IB-B}!xn2W0lO|6xLAUNcU}gIR_NxjHB4g!H*)o{&!0^a&Bj!~+?h*{v6Q zjVGj}vvESUT~jCIaa8%)?1Vd8$`kVbIKzZIt@5eX!%P!Wf0&$*zP;sybn9)Hkk_xN z6XF`DO^8kKslSGrC*O@_ax(I>wDFTeGf#_!mW%@W67Qb(TG^G zVuQBQgGx(n)Mc)ZQ-9!*6BZt_~lpDd@Ue59!=)6A-h2y~NCZit+isVgZ& z1ckUI_(2XmhLAuefd^e)iZlq74F8IbN*In7&NKa{cO>Ez-jgZIi`VTb( zodKF>WEuUt6b$XT3q*=k&(Q!euot?`_3i8j5$ zOVok(o^RBe;5+kP1GEaErp1~-m1!|FqQ&6yJ62x;C`O@{UNi4$M#h8c+eJdE5-xba z>v>xXFrW%tkSg$zUT21kdI(288Qom!;WgXcxiR|0lb+c3Qy-SR2 zQ()GvX1~@K%c*H!M)z2+1daD*&ZfjKSYIdX7^V^f$XdV z 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≠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 z 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

$OFBoXKawxAg@Lwe%;wpm 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

-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

#8-Tmay+;xf`Awhy~cNbN!xK32to{a5v84y=g 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 z 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

t>%GHxX4 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%0DGdGr>KKM7HHrM_ob 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

f3g=x~{{_Z=aBM`qwps6&$HpkkVgjR+ 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

>)$I_mC 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�HxuRWMd)(PMbc@}9-Tux~p) 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 literal 0 HcmV?d00001 diff --git a/build_helpers/TA_Lib-0.4.20-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.20-cp38-cp38-win_amd64.whl new file mode 100644 index 0000000000000000000000000000000000000000..de7393fded23db8ad909f90caae35c622b4aa767 GIT binary patch literal 505119 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

{message}
", parse_mode=ParseMode.HTML, callback_path="update_status_table", reload_able=True) @@ -469,8 +467,7 @@ class Telegram(RPCHandler): tablefmt='simple') message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' if(update.callback_query): - query = update.callback_query - self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, + self._update_msg(query=update.callback_query, msg=message, parse_mode=ParseMode.HTML, callback_path="update_daily", reload_able=True) else: @@ -548,8 +545,7 @@ class Telegram(RPCHandler): markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") if(update.callback_query): - query = update.callback_query - self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, + self._update_msg(query=update.callback_query, msg=markdown_msg, callback_path="update_profit", reload_able=True) else: self._send_msg(msg=markdown_msg, callback_path="update_profit", reload_able=True) @@ -640,8 +636,7 @@ class Telegram(RPCHandler): f"\t`{result['symbol']}: " f"{round_coin_value(result['value'], result['symbol'], False)}`\n") if(update.callback_query): - query = update.callback_query - self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, + self._update_msg(query=update.callback_query, msg=output, callback_path="update_balance", reload_able=True) else: self._send_msg(msg=output, callback_path="update_balance", reload_able=True) @@ -841,8 +836,7 @@ class Telegram(RPCHandler): output += stat_line if(sent_messages == 0 and update.callback_query): - query = update.callback_query - self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, + self._update_msg(query=update.callback_query, msg=output, parse_mode=ParseMode.HTML, callback_path="update_performance", reload_able=True) else: @@ -868,8 +862,7 @@ class Telegram(RPCHandler): message = "
{}
".format(message) logger.debug(message) if(update.callback_query): - query = update.callback_query - self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, + self._update_msg(query=update.callback_query, msg=message, parse_mode=ParseMode.HTML, callback_path="update_count", reload_able=True) else: @@ -1106,7 +1099,7 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) - def _update_msg(self, chat_id: str, message_id: str, msg: str, callback_path: str = "", + def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: if reload_able: reply_markup = InlineKeyboardMarkup([ @@ -1115,6 +1108,11 @@ class Telegram(RPCHandler): else: reply_markup = InlineKeyboardMarkup([[]]) msg += "\nUpdated: {}".format(datetime.now().ctime()) + if not query.message: + return + chat_id = query.message.chat_id + message_id = query.message.message_id + try: try: self._updater.bot.edit_message_text( From a95f760ff7e46462ca7116aa5f6616fcc54724e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 20:34:08 +0200 Subject: [PATCH 0652/1386] Simplify update logic by moving it to send_msg --- freqtrade/rpc/telegram.py | 65 +++++++++++++-------------------------- 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 921fdfe59..0fb322eb8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -10,7 +10,7 @@ from datetime import date, datetime, timedelta from html import escape from itertools import chain from math import isnan -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List, Optional, Union import arrow from tabulate import tabulate @@ -421,14 +421,9 @@ class Telegram(RPCHandler): # insert separators line between Total lines = message.split("\n") message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) - if(messages_count == 1 and update.callback_query): - self._update_msg(query=update.callback_query, - msg=f"
{message}
", - parse_mode=ParseMode.HTML, - callback_path="update_status_table", reload_able=True) - else: - self._send_msg(f"
{message}
", reload_able=True, - callback_path="update_status_table", parse_mode=ParseMode.HTML) + self._send_msg(f"
{message}
", reload_able=True, + callback_path="update_status_table", parse_mode=ParseMode.HTML, + query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -466,13 +461,8 @@ class Telegram(RPCHandler): ], tablefmt='simple') message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' - if(update.callback_query): - self._update_msg(query=update.callback_query, - msg=message, parse_mode=ParseMode.HTML, - callback_path="update_daily", reload_able=True) - else: - self._send_msg(msg=message, parse_mode=ParseMode.HTML, callback_path="update_daily", - reload_able=True) + self._send_msg(message, parse_mode=ParseMode.HTML, callback_path="update_daily", + reload_able=True, query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -544,11 +534,8 @@ class Telegram(RPCHandler): if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") - if(update.callback_query): - self._update_msg(query=update.callback_query, - msg=markdown_msg, callback_path="update_profit", reload_able=True) - else: - self._send_msg(msg=markdown_msg, callback_path="update_profit", reload_able=True) + self._send_msg(markdown_msg, callback_path="update_profit", reload_able=True, + query=update.callback_query) @authorized_only def _stats(self, update: Update, context: CallbackContext) -> None: @@ -635,11 +622,8 @@ class Telegram(RPCHandler): f"\t`{result['stake']}: {result['total']: .8f}`\n" f"\t`{result['symbol']}: " f"{round_coin_value(result['value'], result['symbol'], False)}`\n") - if(update.callback_query): - self._update_msg(query=update.callback_query, - msg=output, callback_path="update_balance", reload_able=True) - else: - self._send_msg(msg=output, callback_path="update_balance", reload_able=True) + self._send_msg(output, callback_path="update_balance", reload_able=True, + query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -820,7 +804,6 @@ class Telegram(RPCHandler): try: trades = self._rpc._rpc_performance() output = "Performance:\n" - sent_messages = 0 for i, trade in enumerate(trades): stat_line = ( f"{i+1}.\t {trade['pair']}\t" @@ -831,17 +814,12 @@ class Telegram(RPCHandler): if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line - sent_messages += 1 else: output += stat_line - if(sent_messages == 0 and update.callback_query): - self._update_msg(query=update.callback_query, - msg=output, parse_mode=ParseMode.HTML, - callback_path="update_performance", reload_able=True) - else: - self._send_msg(msg=output, parse_mode=ParseMode.HTML, - callback_path="update_performance", reload_able=True) + self._send_msg(output, parse_mode=ParseMode.HTML, + callback_path="update_performance", reload_able=True, + query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -861,13 +839,9 @@ class Telegram(RPCHandler): tablefmt='simple') message = "
{}
".format(message) logger.debug(message) - if(update.callback_query): - self._update_msg(query=update.callback_query, - msg=message, parse_mode=ParseMode.HTML, - callback_path="update_count", reload_able=True) - else: - self._send_msg(msg=message, parse_mode=ParseMode.HTML, - callback_path="update_count", reload_able=True) + self._send_msg(message, parse_mode=ParseMode.HTML, + callback_path="update_count", reload_able=True, + query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -1140,7 +1114,8 @@ class Telegram(RPCHandler): disable_notification: bool = False, keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None, callback_path: str = "", - reload_able: bool = False) -> None: + reload_able: bool = False, + query: Optional[CallbackQuery] = None) -> None: """ Send given markdown message :param msg: message @@ -1148,6 +1123,10 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ + if query: + self._update_msg(query=query, msg=msg, parse_mode=parse_mode, + callback_path=callback_path, reload_able=reload_able) + return if reload_able and self._config['telegram'].get('reload', True): reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("Refresh", callback_data=callback_path)]]) From e226252921c87370f173631221ca5f522ce1ccba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 20:39:25 +0200 Subject: [PATCH 0653/1386] Always use the same parameter sequence --- freqtrade/rpc/telegram.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0fb322eb8..8f8627ece 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -421,8 +421,8 @@ class Telegram(RPCHandler): # insert separators line between Total lines = message.split("\n") message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) - self._send_msg(f"
{message}
", reload_able=True, - callback_path="update_status_table", parse_mode=ParseMode.HTML, + self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_status_table", query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -461,8 +461,8 @@ class Telegram(RPCHandler): ], tablefmt='simple') message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' - self._send_msg(message, parse_mode=ParseMode.HTML, callback_path="update_daily", - reload_able=True, query=update.callback_query) + 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)) @@ -534,7 +534,7 @@ class Telegram(RPCHandler): if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") - self._send_msg(markdown_msg, callback_path="update_profit", reload_able=True, + self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", query=update.callback_query) @authorized_only @@ -622,7 +622,7 @@ class Telegram(RPCHandler): f"\t`{result['stake']}: {result['total']: .8f}`\n" f"\t`{result['symbol']}: " f"{round_coin_value(result['value'], result['symbol'], False)}`\n") - self._send_msg(output, callback_path="update_balance", reload_able=True, + self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -818,7 +818,7 @@ class Telegram(RPCHandler): output += stat_line self._send_msg(output, parse_mode=ParseMode.HTML, - callback_path="update_performance", reload_able=True, + reload_able=True, callback_path="update_performance", query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -840,7 +840,7 @@ class Telegram(RPCHandler): message = "
{}
".format(message) logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML, - callback_path="update_count", reload_able=True, + reload_able=True, callback_path="update_count", query=update.callback_query) except RPCException as e: self._send_msg(str(e)) From cd6620a044d9ebd53584febc9ef7500e6d2dd820 Mon Sep 17 00:00:00 2001 From: Bernd Zeimetz Date: Fri, 11 Jun 2021 15:01:13 +0200 Subject: [PATCH 0654/1386] Ignore broken symlinks while resolving strategies. Without this fix the resolver tries to read from the broken symlink, resulting in an exception that leads to the the rather confusing error message freqtrade.resolvers.iresolver - WARNING - Path "...../user_data/strategies" does not exist. as a result of a symlink matching .py not being readable. --- freqtrade/resolvers/iresolver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index b51795e9e..5172e6fda 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -91,6 +91,9 @@ class IResolver: if not str(entry).endswith('.py'): logger.debug('Ignoring %s', entry) continue + if entry.is_symlink() and not entry.is_file(): + logger.debug('Ignoring broken symlink %s', entry) + continue module_path = entry.resolve() obj = next(cls._get_valid_object(module_path, object_name), None) From 6dc4259c6e6e664cf3bdb9f14b37447a1522152f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 05:18:54 +0000 Subject: [PATCH 0655/1386] Bump mkdocs-material from 7.1.7 to 7.1.8 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.7 to 7.1.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.7...7.1.8) --- 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 776d5592a..9b27d974b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2 -mkdocs-material==7.1.7 +mkdocs-material==7.1.8 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 From 4530ae28cd0786611926ceb036d002f3956cdfa6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 05:19:16 +0000 Subject: [PATCH 0656/1386] Bump sqlalchemy from 1.4.17 to 1.4.18 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.17 to 1.4.18. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- 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 abfae55b9..fddf2cffe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.51.3 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.17 +SQLAlchemy==1.4.18 python-telegram-bot==13.6 arrow==1.1.0 cachetools==4.2.2 From 3f1d6d453cbde0cceab5b1b2239bce9b5f6b1635 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 05:19:23 +0000 Subject: [PATCH 0657/1386] Bump mypy from 0.812 to 0.902 Bumps [mypy](https://github.com/python/mypy) from 0.812 to 0.902. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.812...v0.902) --- 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 6fbe581a5..b0f970224 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==3.1.0 flake8==3.9.2 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.3.0 -mypy==0.812 +mypy==0.902 pytest==6.2.4 pytest-asyncio==0.15.1 pytest-cov==2.12.1 From fe933e78bde88a6e3424aeafd397fbc311770b36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 05:19:33 +0000 Subject: [PATCH 0658/1386] Bump ccxt from 1.51.3 to 1.51.40 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.51.3 to 1.51.40. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.51.3...1.51.40) --- 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 abfae55b9..781425ec5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.3 pandas==1.2.4 -ccxt==1.51.3 +ccxt==1.51.40 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From 63802aa7f66487d8a83342e757eaf69da89bc207 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 08:52:42 +0000 Subject: [PATCH 0659/1386] Bump mkdocs from 1.2 to 1.2.1 Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.2 to 1.2.1. - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.2...1.2.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 9b27d974b..352103f8b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ -mkdocs==1.2 +mkdocs==1.2.1 mkdocs-material==7.1.8 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 From 4ba7a2bbd290c2c6e5746995ceacd89660a04ab5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Jun 2021 19:18:42 +0200 Subject: [PATCH 0660/1386] Fix mypy update problems --- freqtrade/resolvers/iresolver.py | 3 +++ freqtrade/rpc/webhook.py | 15 +++++++-------- requirements-dev.txt | 6 ++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 5172e6fda..5b6977b4b 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -58,6 +58,9 @@ class IResolver: # Generate spec based on absolute path # Pass object_name as first argument to have logging print a reasonable name. spec = importlib.util.spec_from_file_location(object_name or "", str(module_path)) + if not spec: + return iter([None]) + module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 0e4a4bf6f..b4c55649e 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -77,14 +77,13 @@ class Webhook(RPCHandler): def _send_msg(self, payload: dict) -> None: """do the actual call to the webhook""" - if self._format == 'form': - kwargs = {'data': payload} - elif self._format == 'json': - kwargs = {'json': payload} - else: - raise NotImplementedError('Unknown format: {}'.format(self._format)) - try: - post(self._url, **kwargs) + if self._format == 'form': + post(self._url, data=payload) + elif self._format == 'json': + post(self._url, json=payload) + else: + raise NotImplementedError('Unknown format: {}'.format(self._format)) + except RequestException as exc: logger.warning("Could not call webhook url. Exception: %s", exc) diff --git a/requirements-dev.txt b/requirements-dev.txt index b0f970224..924b35e1a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,3 +17,9 @@ isort==5.8.0 # Convert jupyter notebooks to markdown documents nbconvert==6.0.7 + +# mypy types +types-cachetools==0.1.7 +types-filelock==0.1.3 +types-requests==0.1.11 +types-tabulate==0.1.0 From cf7394d01cb6798213bb4b08572885328756adda Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Jun 2021 19:57:24 +0200 Subject: [PATCH 0661/1386] Export backtesting results by default closes #4977 --- docs/backtesting.md | 21 +++++++++++++-------- freqtrade/commands/cli_options.py | 5 +++-- freqtrade/constants.py | 2 ++ freqtrade/optimize/backtesting.py | 2 +- tests/conftest.py | 1 + tests/optimize/test_backtesting.py | 12 ++++++------ tests/test_configuration.py | 5 ++--- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2027c2079..26642ef8c 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -19,7 +19,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] - [--export EXPORT] [--export-filename PATH] + [--export {none,trades}] [--export-filename PATH] optional arguments: -h, --help show this help message and exit @@ -63,8 +63,8 @@ optional arguments: name is injected into the filename (so `backtest- data.json` becomes `backtest-data- DefaultStrategy.json` - --export EXPORT Export backtest results, argument are: trades. - Example: `--export=trades` + --export {none,trades} + Export backtest results (default: trades). --export-filename PATH Save backtest results to the file with this filename. Requires `--export` to be set as well. Example: @@ -100,7 +100,7 @@ Strategy arguments: Now you have good Buy and Sell strategies and some historic data, you want to test it against real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). -Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/` by default. +Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHLCV) data from `user_data/data/` by default. If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`. For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation. @@ -110,11 +110,16 @@ All profit calculations include fees, and freqtrade will use the exchange's defa !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. - Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. + Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed. Please read the [pairlists documentation](plugins.md#pairlists) for more information. To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. +!!! Note + By default, Freqtrade will export backtesting results to `user_data/backtest_results`. + The exported trades can be used for [further analysis](#further-backtest-result-analysis) or can be used by the [plotting sub-command](plotting.md#plot-price-and-indicators) (`freqtrade plot-dataframe`) in the scripts directory. + + ### Starting balance Backtesting will require a starting balance, which can be provided as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. @@ -174,13 +179,13 @@ Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies --- -Exporting trades to file +Prevent exporting trades to file ```bash -freqtrade backtesting --strategy backtesting --export trades --config config.json +freqtrade backtesting --strategy backtesting --export none --config config.json ``` -The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. +Only use this if you're sure you'll not want to plot or analyze your results further. --- diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index d832693ee..b226415e7 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -167,8 +167,9 @@ AVAILABLE_CLI_OPTIONS = { ), "export": Arg( '--export', - help='Export backtest results, argument are: trades. ' - 'Example: `--export=trades`', + help='Export backtest results (default: trades).', + choices=constants.EXPORT_OPTIONS, + ), "exportfilename": Arg( '--export-filename', diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e42b9d4b8..259aa0e03 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -12,6 +12,7 @@ PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec TIMEOUT_UNITS = ['minutes', 'seconds'] +EXPORT_OPTIONS = ['none', 'trades'] DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -308,6 +309,7 @@ CONF_SCHEMA = { 'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password'] }, 'db_url': {'type': 'string'}, + 'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'forcebuy_enable': {'type': 'boolean'}, 'disable_dataframe_checks': {'type': 'boolean'}, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 922f89c22..c72a8b5c5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -520,7 +520,7 @@ class Backtesting: stats = generate_backtest_stats(data, self.all_results, min_date=min_date, max_date=max_date) - if self.config.get('export', False): + if self.config.get('export', 'none') == 'trades': store_backtest_stats(self.config['exportfilename'], stats) # Show backtest results diff --git a/tests/conftest.py b/tests/conftest.py index dd38ca610..c6a0dfcfd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -326,6 +326,7 @@ def get_default_conf(testdatadir): "strategy_path": str(Path(__file__).parent / "strategy" / "strats"), "strategy": "DefaultStrategy", "internals": {}, + "export": "none", } return configuration diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 7387c8865..60bd82d71 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -155,6 +155,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', + '--export', 'none' ] config = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) @@ -172,7 +173,8 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca assert not log_has('Parameter --enable-position-stacking detected ...', caplog) assert 'timerange' not in config - assert 'export' not in config + assert 'export' in config + assert config['export'] == 'none' assert 'runmode' in config assert config['runmode'] == RunMode.BACKTEST @@ -193,7 +195,6 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> '--enable-position-stacking', '--disable-max-market-positions', '--timerange', ':100', - '--export', '/bar/foo', '--export-filename', 'foo_bar.json', '--fee', '0', ] @@ -223,7 +224,6 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog) assert 'export' in config - assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) assert 'exportfilename' in config assert isinstance(config['exportfilename'], Path) assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog) @@ -395,7 +395,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> default_conf['timeframe'] = "1m" default_conf['datadir'] = testdatadir - default_conf['export'] = None + default_conf['export'] = 'none' default_conf['timerange'] = '20180101-20180102' backtesting = Backtesting(default_conf) @@ -416,7 +416,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> default_conf['timeframe'] = "1m" default_conf['datadir'] = testdatadir - default_conf['export'] = None + default_conf['export'] = 'none' default_conf['timerange'] = '20180101-20180102' with pytest.raises(OperationalException, match='No pair in whitelist.'): @@ -440,7 +440,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti default_conf['ticker_interval'] = "1m" default_conf['datadir'] = testdatadir - default_conf['export'] = None + default_conf['export'] = 'none' # Use stoploss from strategy del default_conf['stoploss'] default_conf['timerange'] = '20180101-20180102' diff --git a/tests/test_configuration.py b/tests/test_configuration.py index aa121edfa..c5d0cd908 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -425,7 +425,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert not log_has('Parameter --enable-position-stacking detected ...', caplog) assert 'timerange' not in config - assert 'export' not in config def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: @@ -448,7 +447,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non '--enable-position-stacking', '--disable-max-market-positions', '--timerange', ':100', - '--export', '/bar/foo', + '--export', 'trades', '--stake-amount', 'unlimited' ] @@ -496,7 +495,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non 'backtesting', '--config', 'config.json', '--ticker-interval', '1m', - '--export', '/bar/foo', + '--export', 'trades', '--strategy-list', 'DefaultStrategy', 'TestStrategy' From 6d5fc967147e0aae37423d1e0ee57de47a17a791 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 12 Jun 2021 10:16:30 +0300 Subject: [PATCH 0662/1386] Implement most pessimistic handling of trailing stoploss. --- freqtrade/optimize/backtesting.py | 16 +++++++++ freqtrade/strategy/interface.py | 4 +-- tests/optimize/test_backtest_detail.py | 47 ++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c72a8b5c5..19ae74ae6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -225,6 +225,22 @@ class Backtesting: # sell at open price. return sell_row[OPEN_IDX] + # Special case: trailing triggers within same candle as trade opened. Assume most + # pessimistic price movement, which is moving just enough to arm stoploss and + # immediately going down to stop price. + if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0 and \ + self.strategy.trailing_stop_positive: + if self.strategy.trailing_only_offset_is_reached: + # Worst case: price reaches stop_positive_offset and dives down. + stop_rate = sell_row[OPEN_IDX] * \ + (1 + abs(self.strategy.trailing_stop_positive_offset) - + abs(self.strategy.trailing_stop_positive)) + else: + # Worst case: price ticks tiny bit above open and dives down. + stop_rate = sell_row[OPEN_IDX] * (1 - abs(self.strategy.trailing_stop_positive)) + assert stop_rate < sell_row[HIGH_IDX] + return stop_rate + # Set close_rate to stoploss return trade.stop_loss elif sell.sell_type == (SellType.ROI): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8ea38f503..47d4259fc 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -610,7 +610,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) - if self.use_custom_stoploss: + if self.use_custom_stoploss and trade.stop_loss < current_rate: stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, @@ -623,7 +623,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: logger.warning("CustomStoploss function did not return valid stoploss") - if self.trailing_stop: + if self.trailing_stop and trade.stop_loss < current_rate: # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e5b969383..488425323 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -457,6 +457,50 @@ tc28 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) +# Test 29: trailing_stop should be triggered by low of next candle, without adjusting stoploss using +# high of stoploss candle. +# stop-loss: 10%, ROI: 10% (should not apply) +tc29 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Triggers trailing-stoploss + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.02, trailing_stop=True, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] +) + +# Test 30: trailing_stop should be triggered immediately on trade open candle. +# stop-loss: 10%, ROI: 10% (should not apply) +tc30 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, + trailing_stop_positive=0.01, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] +) + +# Test 31: trailing_stop should be triggered immediately on trade open candle. +# stop-loss: 10%, ROI: 10% (should not apply) +tc31 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.01, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, + trailing_stop_positive=0.01, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] +) + TESTS = [ tc0, tc1, @@ -487,6 +531,9 @@ TESTS = [ tc26, tc27, tc28, + tc29, + tc30, + tc31, ] From 38ed49cef54d0bfe4606be46e2ec88c344e768b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 16:37:11 +0200 Subject: [PATCH 0663/1386] move low to stoploss_reached to clarify where which rate is used --- freqtrade/strategy/interface.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 47d4259fc..6358c6a4e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -524,15 +524,14 @@ class IStrategy(ABC, HyperStrategyMixin): :param force_stoploss: Externally provided stoploss :return: True if trade should be sold, False otherwise """ - # Set current rate to low for backtesting sell - current_rate = low or rate + current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate) stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss, high=high) + force_stoploss=force_stoploss, low=low, high=high) # Set current rate to high for backtesting sell current_rate = high or rate @@ -599,18 +598,21 @@ class IStrategy(ABC, HyperStrategyMixin): def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, - force_stoploss: float, high: float = None) -> SellCheckTuple: + force_stoploss: float, low: float = None, + high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not :param current_profit: current profit as ratio + :param low: Low value of this candle, only set in backtesting + :param high: High value of this candle, only set in backtesting """ stop_loss_value = force_stoploss if force_stoploss else self.stoploss # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) - if self.use_custom_stoploss and trade.stop_loss < current_rate: + if self.use_custom_stoploss and trade.stop_loss < (low or current_rate): stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, @@ -623,7 +625,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: logger.warning("CustomStoploss function did not return valid stoploss") - if self.trailing_stop and trade.stop_loss < current_rate: + if self.trailing_stop and trade.stop_loss < (low or current_rate): # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset @@ -643,7 +645,7 @@ class IStrategy(ABC, HyperStrategyMixin): # 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. - if ((trade.stop_loss >= current_rate) and + if ((trade.stop_loss >= (low or current_rate)) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS @@ -652,7 +654,7 @@ class IStrategy(ABC, HyperStrategyMixin): if trade.initial_stop_loss != trade.stop_loss: sell_type = SellType.TRAILING_STOP_LOSS logger.debug( - f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, " + f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, " 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}") From 1bb04bb0c24c8a959df1fbc77617f5e1c1752d29 Mon Sep 17 00:00:00 2001 From: barbarius Date: Wed, 16 Jun 2021 11:40:55 +0200 Subject: [PATCH 0664/1386] Moved daily avg trade row next to total trades on backtest results --- docs/backtesting.md | 8 +++----- freqtrade/optimize/optimize_reports.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 26642ef8c..8e50aa356 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -284,7 +284,7 @@ A backtesting result will look like that: | Backtesting to | 2019-05-01 00:00:00 | | Max open trades | 3 | | | | -| Total trades | 429 | +| Total/Daily Avg Trades| 429 / 3.575 | | Starting balance | 0.01000000 BTC | | Final balance | 0.01762792 BTC | | Absolute profit | 0.00762792 BTC | @@ -373,12 +373,11 @@ It contains some useful key metrics about performance of your strategy on backte | Backtesting to | 2019-05-01 00:00:00 | | Max open trades | 3 | | | | -| Total trades | 429 | +| Total/Daily Avg Trades| 429 / 3.575 | | Starting balance | 0.01000000 BTC | | Final balance | 0.01762792 BTC | | Absolute profit | 0.00762792 BTC | | Total profit % | 76.2% | -| Trades per day | 3.575 | | Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | @@ -409,12 +408,11 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). -- `Total trades`: Identical to the total trades of the backtest output table. +- `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Starting balance`: Start balance - as given by dry-run-wallet (config or command line). - `Final balance`: Final balance - starting balance + absolute profit. - `Absolute profit`: Profit made in stake currency. - `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. -- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 84e052ac4..64b043304 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -556,7 +556,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Backtesting to', strat_results['backtest_end']), ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability - ('Total trades', strat_results['total_trades']), + ('Total/Daily Avg Trades', + f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"), ('Starting balance', round_coin_value(strat_results['starting_balance'], strat_results['stake_currency'])), ('Final balance', round_coin_value(strat_results['final_balance'], @@ -564,7 +565,6 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"), - ('Trades per day', strat_results['trades_per_day']), ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'], strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], From 1c9def2fdbfad2e11a283b900cde6e8968145930 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Jun 2021 20:17:44 +0100 Subject: [PATCH 0665/1386] Update freqtrade/optimize/optimize_reports.py --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 64b043304..df7f721ec 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -556,7 +556,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Backtesting to', strat_results['backtest_end']), ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability - ('Total/Daily Avg Trades', + ('Total/Daily Avg Trades', f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"), ('Starting balance', round_coin_value(strat_results['starting_balance'], strat_results['stake_currency'])), From b38ab84a13d23906b05b236f6eb9421088b9c09a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Jun 2021 06:48:41 +0200 Subject: [PATCH 0666/1386] Add documentation mention about new behaviour --- docs/backtesting.md | 1 + freqtrade/optimize/backtesting.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 26642ef8c..d34381f55 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -446,6 +446,7 @@ Since backtesting lacks some detailed information about what happens within a ca - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Low happens before high for stoploss, protecting capital first - Trailing stoploss + - Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered) - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 19ae74ae6..028a9eacd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -228,13 +228,13 @@ class Backtesting: # Special case: trailing triggers within same candle as trade opened. Assume most # pessimistic price movement, which is moving just enough to arm stoploss and # immediately going down to stop price. - if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0 and \ - self.strategy.trailing_stop_positive: + if (sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0 + and self.strategy.trailing_stop_positive): if self.strategy.trailing_only_offset_is_reached: # Worst case: price reaches stop_positive_offset and dives down. - stop_rate = sell_row[OPEN_IDX] * \ - (1 + abs(self.strategy.trailing_stop_positive_offset) - - abs(self.strategy.trailing_stop_positive)) + stop_rate = (sell_row[OPEN_IDX] * + (1 + abs(self.strategy.trailing_stop_positive_offset) - + abs(self.strategy.trailing_stop_positive))) else: # Worst case: price ticks tiny bit above open and dives down. stop_rate = sell_row[OPEN_IDX] * (1 - abs(self.strategy.trailing_stop_positive)) From a49ca9cbf78b1a0e458c3e4767344d2c71a66219 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Jun 2021 06:57:35 +0200 Subject: [PATCH 0667/1386] Change log-level "Executing handler" msg to debug closes #5143 --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c3ddfd644..aee513017 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -56,7 +56,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: ) return wrapper - logger.info( + logger.debug( 'Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bbda55c3e..d091f3837 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2,6 +2,7 @@ # pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=too-many-lines, too-many-arguments +import logging import re from datetime import datetime from functools import reduce @@ -120,7 +121,7 @@ def test_cleanup(default_conf, mocker, ) -> None: def test_authorized_only(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) - + caplog.set_level(logging.DEBUG) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) rpc = RPC(bot) @@ -136,6 +137,7 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None: def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: patch_exchange(mocker) + caplog.set_level(logging.DEBUG) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) update.message = Message(randint(1, 100), datetime.utcnow(), chat) From a9f111dca0063790dadaebfad03c265c8e0842ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Jun 2021 19:50:49 +0200 Subject: [PATCH 0668/1386] Fix some types --- freqtrade/rpc/telegram.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f83d5a238..6a0e98a75 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -95,7 +95,7 @@ class Telegram(RPCHandler): Validates the keyboard configuration from telegram config section. """ - self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [ + self._keyboard: List[List[Union[str, KeyboardButton]]] = [ ['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help'] @@ -1112,7 +1112,7 @@ class Telegram(RPCHandler): def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None, + keyboard: List[List[InlineKeyboardButton]] = None, callback_path: str = "", reload_able: bool = False, query: Optional[CallbackQuery] = None) -> None: @@ -1123,6 +1123,7 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ + reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] if query: self._update_msg(query=query, msg=msg, parse_mode=parse_mode, callback_path=callback_path, reload_able=reload_able) From 8562e19776433d182d7406ad1594ed2220d37ae9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Jun 2021 20:15:53 +0200 Subject: [PATCH 0669/1386] Document protections to come from the strategy --- docs/configuration.md | 3 +- docs/includes/protections.md | 67 +++++++----------------------------- 2 files changed, 14 insertions(+), 56 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index ef6f34094..3788ef57c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -105,7 +105,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts -| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** List of Dicts +| `protections` | Define one or more protections to be used. [More information](plugins.md#protections).
**Datatype:** List of Dicts | `telegram.enabled` | Enable the usage of Telegram.
**Datatype:** Boolean | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String @@ -156,7 +156,6 @@ Values set in the configuration file always overwrite values set in the strategy * `order_time_in_force` * `unfilledtimeout` * `disable_dataframe_checks` -* `protections` * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `sell_profit_offset` (ask_strategy) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 6bc57153e..3ea2dde61 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -8,7 +8,6 @@ All protection end times are rounded up to the next candle to avoid sudden, unex !!! Note Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance. - To align your protection with your strategy, you can define protections in the strategy. !!! Tip Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). @@ -47,16 +46,16 @@ This applies across all pairs, unless `only_per_pair` is set to true, which will 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. -```json -"protections": [ +``` python +protections = [ { "method": "StoplossGuard", "lookback_period_candles": 24, "trade_limit": 4, "stop_duration_candles": 4, - "only_per_pair": false + "only_per_pair": False } -], +] ``` !!! Note @@ -69,8 +68,8 @@ The below example stops trading for all pairs for 4 candles after the last trade The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. -```json -"protections": [ +``` python +protections = [ { "method": "MaxDrawdown", "lookback_period_candles": 48, @@ -78,7 +77,7 @@ The below sample stops trading for 12 candles if max-drawdown is > 20% consideri "stop_duration_candles": 12, "max_allowed_drawdown": 0.2 }, -], +] ``` #### Low Profit Pairs @@ -88,8 +87,8 @@ If that ratio is below `required_profit`, that pair will be locked for `stop_dur The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles. -```json -"protections": [ +``` python +protections = [ { "method": "LowProfitPairs", "lookback_period_candles": 6, @@ -97,7 +96,7 @@ The below example will stop trading a pair for 60 minutes if the pair does not h "stop_duration": 60, "required_profit": 0.02 } -], +] ``` #### Cooldown Period @@ -106,13 +105,13 @@ The below example will stop trading a pair for 60 minutes if the pair does not h The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down". -```json -"protections": [ +``` python +protections = [ { "method": "CooldownPeriod", "stop_duration_candles": 2 } -], +] ``` !!! Note @@ -132,46 +131,6 @@ The below example assumes a timeframe of 1 hour: * Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). * Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades. -```json -"timeframe": "1h", -"protections": [ - { - "method": "CooldownPeriod", - "stop_duration_candles": 5 - }, - { - "method": "MaxDrawdown", - "lookback_period_candles": 48, - "trade_limit": 20, - "stop_duration_candles": 4, - "max_allowed_drawdown": 0.2 - }, - { - "method": "StoplossGuard", - "lookback_period_candles": 24, - "trade_limit": 4, - "stop_duration_candles": 2, - "only_per_pair": false - }, - { - "method": "LowProfitPairs", - "lookback_period_candles": 6, - "trade_limit": 2, - "stop_duration_candles": 60, - "required_profit": 0.02 - }, - { - "method": "LowProfitPairs", - "lookback_period_candles": 24, - "trade_limit": 4, - "stop_duration_candles": 2, - "required_profit": 0.01 - } - ], -``` - -You can use the same in your strategy, the syntax is only slightly different: - ``` python from freqtrade.strategy import IStrategy From 546ca0107178f0a95b41c433aeb3e6497c9f6a48 Mon Sep 17 00:00:00 2001 From: Rik Helsen Date: Thu, 17 Jun 2021 20:33:21 +0200 Subject: [PATCH 0670/1386] :recycle: Fixed flake8 warning --- freqtrade/optimize/hyperopt_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 92ec6f194..742db07cc 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -114,7 +114,8 @@ class HyperoptTools(): if len(space_non_optimized) > 0: for non_optimized_param in space_non_optimized: if non_optimized_param not in all_space_params: - all_space_params[non_optimized_param] = space_non_optimized[non_optimized_param] + all_space_params[non_optimized_param] = \ + space_non_optimized[non_optimized_param] if space in ['buy', 'sell']: result_dict.setdefault('params', {}).update(all_space_params) From 15678045096f75b26449dcb964c9d579654e41ea Mon Sep 17 00:00:00 2001 From: Rik Helsen Date: Thu, 17 Jun 2021 22:41:49 +0200 Subject: [PATCH 0671/1386] :zap: kwargs merge dictionaries instead of using loops --- freqtrade/optimize/hyperopt_tools.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 742db07cc..dac299dc6 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -110,12 +110,9 @@ class HyperoptTools(): space_non_optimized = HyperoptTools._space_params(non_optimized, space) all_space_params = space_params - # Include non optimized params if there are any + # Merge non optimized params if there are any if len(space_non_optimized) > 0: - for non_optimized_param in space_non_optimized: - if non_optimized_param not in all_space_params: - all_space_params[non_optimized_param] = \ - space_non_optimized[non_optimized_param] + all_space_params = {**space_non_optimized, **space_params} if space in ['buy', 'sell']: result_dict.setdefault('params', {}).update(all_space_params) From 0a1e15988f9149670a8c7df2f77ad2a04a20e046 Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Fri, 18 Jun 2021 09:48:59 +0200 Subject: [PATCH 0672/1386] Fix errors during ubuntu install Encountering the python header error on a fresh ubuntu install: ``` utils_find_1st/find_1st.cpp:3:10: fatal error: Python.h: No such file or directory #include "Python.h" ^~~~~~~~~~ compilation terminated. ``` solved by installing python3.7-dev. Also need to ensure python3.7-venv for fresh install. --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index c19965a18..25994fdc0 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces sudo apt-get update # install packages - sudo apt install -y python3-pip python3-venv python3-pandas git + sudo apt install -y python3-pip python3.7-venv python3.7-dev python3-pandas git ``` === "RaspberryPi/Raspbian" From e1010ff5923e4c68900c6e786bb3e6138ea1265c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Jun 2021 21:01:22 +0200 Subject: [PATCH 0673/1386] Don't load protections from config if strategy defines a property --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/plugins/protectionmanager.py | 4 ++-- freqtrade/resolvers/strategy_resolver.py | 4 +++- freqtrade/strategy/interface.py | 2 +- tests/plugins/test_protections.py | 3 +-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a2e7fcb5d..e8a321e94 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -70,7 +70,7 @@ class FreqtradeBot(LoggingMixin): PairLocks.timeframe = self.config['timeframe'] - self.protections = ProtectionManager(self.config) + 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, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 028a9eacd..8b75fe438 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -137,7 +137,7 @@ class Backtesting: if hasattr(strategy, 'protections'): conf = deepcopy(conf) conf['protections'] = strategy.protections - self.protections = ProtectionManager(conf) + self.protections = ProtectionManager(self.config, strategy.protections) def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]: """ diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index a8edd4e4b..f33e5b4bc 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -15,11 +15,11 @@ logger = logging.getLogger(__name__) class ProtectionManager(): - def __init__(self, config: dict) -> None: + def __init__(self, config: Dict, protections: List) -> None: self._config = config self._protection_handlers: List[IProtection] = [] - for protection_handler_config in self._config.get('protections', []): + for protection_handler_config in protections: protection_handler = ProtectionResolver.load_protection( protection_handler_config['method'], config=config, diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 6484f900b..e76d1e3e5 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -113,7 +113,9 @@ class StrategyResolver(IResolver): - Strategy - default (if not None) """ - if attribute in config: + if (attribute in config + and not isinstance(getattr(type(strategy), 'my_property', None), property)): + # Ensure Properties are not overwritten setattr(strategy, attribute, config[attribute]) logger.info("Override strategy '%s' with value in config file: %s.", attribute, config[attribute]) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6358c6a4e..b259a7977 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -107,7 +107,7 @@ class IStrategy(ABC, HyperStrategyMixin): startup_candle_count: int = 0 # Protections - protections: List + protections: List = [] # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 10ab64690..9ec47dade 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -70,8 +70,7 @@ def test_protectionmanager(mocker, default_conf): ]) def test_protections_init(mocker, default_conf, timeframe, expected, protconf): default_conf['timeframe'] = timeframe - default_conf['protections'] = protconf - man = ProtectionManager(default_conf) + man = ProtectionManager(default_conf, protconf) assert len(man._protection_handlers) == len(protconf) assert man._protection_handlers[0]._lookback_period == expected[0] assert man._protection_handlers[0]._stop_duration == expected[1] From 6e89fbd14665f4aa5473af33a7cd1b64ec0f0e0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Jun 2021 21:06:58 +0200 Subject: [PATCH 0674/1386] Remove Dockerfile.aarch64 it's identical to the real image except for the "--platform" tag, which is unnecessary if building from a arm64 architecture --- docker/Dockerfile.aarch64 | 58 --------------------------------------- docs/docker_quickstart.md | 2 +- 2 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 docker/Dockerfile.aarch64 diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 deleted file mode 100644 index e5d3f0ee9..000000000 --- a/docker/Dockerfile.aarch64 +++ /dev/null @@ -1,58 +0,0 @@ -FROM --platform=linux/arm64/v8 python:3.9.4-slim-buster as base - -# Setup env -ENV LANG C.UTF-8 -ENV LC_ALL C.UTF-8 -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONFAULTHANDLER 1 -ENV PATH=/home/ftuser/.local/bin:$PATH -ENV FT_APP_ENV="docker" - -# Prepare environment -RUN mkdir /freqtrade \ - && apt-get update \ - && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \ - && apt-get clean \ - && useradd -u 1000 -G sudo -U -m ftuser \ - && chown ftuser:ftuser /freqtrade \ - # Allow sudoers - && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers - -WORKDIR /freqtrade - -# Install dependencies -FROM base as python-deps -RUN apt-get update \ - && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ - && apt-get clean \ - && pip install --upgrade pip - -# Install TA-lib -COPY build_helpers/* /tmp/ -RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* -ENV LD_LIBRARY_PATH /usr/local/lib - -# Install dependencies -COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ -USER ftuser -RUN pip install --user --no-cache-dir numpy \ - && pip install --user --no-cache-dir -r requirements-hyperopt.txt - -# Copy dependencies to runtime-image -FROM base as runtime-image -COPY --from=python-deps /usr/local/lib /usr/local/lib -ENV LD_LIBRARY_PATH /usr/local/lib - -COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local - -USER ftuser -# Install and execute -COPY --chown=ftuser:ftuser . /freqtrade/ - -RUN pip install -e . --user --no-cache-dir --no-build-isolation\ - && mkdir /freqtrade/user_data/ \ - && freqtrade install-ui - -ENTRYPOINT ["freqtrade"] -# Default to trade mode -CMD [ "trade" ] diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 3a85aa885..cb66fc7e2 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -98,7 +98,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse image: freqtradeorg/freqtrade:custom_arm64 build: context: . - dockerfile: "./docker/Dockerfile.aarch64" + dockerfile: "Dockerfile" ``` The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. From 656bebd4da833dc008b3487ebbe4d7cb134c1d64 Mon Sep 17 00:00:00 2001 From: Rik Helsen Date: Fri, 18 Jun 2021 22:03:04 +0200 Subject: [PATCH 0675/1386] :beetle: Included completely non_optimized spaces in json + swapped merge dictionary order --- freqtrade/optimize/hyperopt_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index dac299dc6..9eee42a8d 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -105,14 +105,14 @@ class HyperoptTools(): @staticmethod def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None: - if space in params: + if (space in params) or (space in non_optimized): space_params = HyperoptTools._space_params(params, space) space_non_optimized = HyperoptTools._space_params(non_optimized, space) all_space_params = space_params # Merge non optimized params if there are any if len(space_non_optimized) > 0: - all_space_params = {**space_non_optimized, **space_params} + all_space_params = {**space_params, **space_non_optimized} if space in ['buy', 'sell']: result_dict.setdefault('params', {}).update(all_space_params) From 39b876e37a674384fb9e6d4fbc6a777ddec38acd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Jun 2021 20:09:25 +0200 Subject: [PATCH 0676/1386] Log exchange responses if configured --- docs/configuration.md | 1 + freqtrade/exchange/binance.py | 1 + freqtrade/exchange/exchange.py | 21 +++++++++++++++++---- freqtrade/exchange/ftx.py | 7 ++++++- freqtrade/exchange/kraken.py | 1 + tests/exchange/test_exchange.py | 4 +++- 6 files changed, 29 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 3788ef57c..8b85e9e96 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -102,6 +102,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean | `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`
**Datatype:** Boolean | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0bcfa5e17..0c470cb24 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -68,6 +68,7 @@ class Binance(Exchange): amount=amount, price=rate, params=params) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) + self._log_exchange_response('create_stoploss_order', order) return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 67676d4e0..07ac337fc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -104,6 +104,7 @@ class Exchange: logger.info('Instance is running with dry_run enabled') logger.info(f"Using CCXT {ccxt.__version__}") exchange_config = config['exchange'] + self.log_responses = exchange_config.get('log_responses', False) # Deep merge ft_has with default ft_has options self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default)) @@ -226,6 +227,11 @@ class Exchange: """exchange ccxt precisionMode""" return self._api.precisionMode + def _log_exchange_response(self, endpoint, response) -> None: + """ Log exchange responses """ + if self.log_responses: + logger.info(f"API {endpoint}: {response}") + def ohlcv_candle_limit(self, timeframe: str) -> int: """ Exchange ohlcv candle limit @@ -622,8 +628,10 @@ class Exchange: or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None - return self._api.create_order(pair, ordertype, side, - amount, rate_for_order, params) + order = self._api.create_order(pair, ordertype, side, + amount, rate_for_order, params) + self._log_exchange_response('create_order', order) + return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( @@ -694,7 +702,9 @@ class Exchange: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) try: - return self._api.fetch_order(order_id, pair) + order = self._api.fetch_order(order_id, pair) + self._log_exchange_response('fetch_order', order) + return order except ccxt.OrderNotFound as e: raise RetryableOrderError( f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e @@ -744,7 +754,9 @@ class Exchange: return {} try: - return self._api.cancel_order(order_id, pair) + order = self._api.cancel_order(order_id, pair) + self._log_exchange_response('cancel_order', order) + return order except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e @@ -1042,6 +1054,7 @@ class Exchange: pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000)) matched_trades = [trade for trade in my_trades if trade['order'] == order_id] + self._log_exchange_response('get_trades_for_order', matched_trades) return matched_trades except ccxt.DDoSProtection as e: raise DDosProtection(e) from e diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 3184c2524..6cd549d60 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -69,6 +69,7 @@ class Ftx(Exchange): order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, params=params) + self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) return order @@ -99,12 +100,14 @@ class Ftx(Exchange): orders = self._api.fetch_orders(pair, None, params={'type': 'stop'}) order = [order for order in orders if order['id'] == order_id] + self._log_exchange_response('fetch_stoploss_order', order) if len(order) == 1: if order[0].get('status') == 'closed': # Trigger order was triggered ... real_order_id = order[0].get('info', {}).get('orderId') order1 = self._api.fetch_order(real_order_id, pair) + self._log_exchange_response('fetch_stoploss_order1', order1) # Fake type to stop - as this was really a stop order. order1['id_stop'] = order1['id'] order1['id'] = order_id @@ -131,7 +134,9 @@ class Ftx(Exchange): if self._config['dry_run']: return {} try: - return self._api.cancel_order(order_id, pair, params={'type': 'stop'}) + order = self._api.cancel_order(order_id, pair, params={'type': 'stop'}) + self._log_exchange_response('cancel_stoploss_order', order) + return order except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 6f1fa409a..8f7cbe590 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -103,6 +103,7 @@ class Kraken(Exchange): order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, price=stop_price, params=params) + self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) return order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5fa94e6c1..f5becc274 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2271,8 +2271,9 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_fetch_order(default_conf, mocker, exchange_name): +def test_fetch_order(default_conf, mocker, exchange_name, caplog): default_conf['dry_run'] = True + default_conf['exchange']['log_responses'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) @@ -2287,6 +2288,7 @@ def test_fetch_order(default_conf, mocker, exchange_name): api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.fetch_order('X', 'TKN/BTC') == 456 + assert log_has("API fetch_order: 456", caplog) with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) From 6e99e3fbbbb903b8e91cb5373b55e9309f151c56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Jun 2021 09:31:34 +0200 Subject: [PATCH 0677/1386] Implement tests for message updating --- freqtrade/rpc/telegram.py | 33 +++++++++++++-------------------- tests/rpc/test_rpc_telegram.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6a0e98a75..6cb48aef1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1088,27 +1088,20 @@ class Telegram(RPCHandler): message_id = query.message.message_id try: - try: - self._updater.bot.edit_message_text( - chat_id=chat_id, - message_id=message_id, - text=msg, - parse_mode=parse_mode, - reply_markup=reply_markup - ) - except BadRequest as e: - if 'not modified' in e.message.lower(): - pass - else: - logger.warning( - 'TelegramError: %s', - e.message - ) - except TelegramError as telegram_err: - logger.warning( - 'TelegramError: %s! Giving up on that message.', - telegram_err.message + self._updater.bot.edit_message_text( + chat_id=chat_id, + message_id=message_id, + text=msg, + parse_mode=parse_mode, + reply_markup=reply_markup ) + except BadRequest as e: + if 'not modified' in e.message.lower(): + pass + else: + logger.warning('TelegramError: %s', e.message) + except TelegramError as telegram_err: + logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message) def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 830ef200e..39ef6a1ab 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -13,7 +13,7 @@ from unittest.mock import ANY, MagicMock import arrow import pytest from telegram import Chat, Message, ReplyKeyboardMarkup, Update -from telegram.error import NetworkError +from telegram.error import BadRequest, NetworkError, TelegramError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON @@ -25,8 +25,8 @@ from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC from freqtrade.rpc.telegram import Telegram, authorized_only -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, - patch_get_signal, patch_whitelist) +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, + patch_exchange, patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -1561,7 +1561,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): assert telegram._get_sell_emoji(msg) == expected -def test__send_msg(default_conf, mocker) -> None: +def test_telegram__send_msg(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) @@ -1572,6 +1572,28 @@ def test__send_msg(default_conf, mocker) -> None: telegram._send_msg('test') assert len(bot.method_calls) == 1 + # Test update + query = MagicMock() + telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True) + edit_message_text = telegram._updater.bot.edit_message_text + assert edit_message_text.call_count == 1 + assert "Updated: " in edit_message_text.call_args_list[0][1]['text'] + + telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("not modified")) + telegram._send_msg('test', callback_path="DeadBeef", query=query) + assert telegram._updater.bot.edit_message_text.call_count == 1 + assert not log_has_re(r"TelegramError: .*", caplog) + + telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("")) + telegram._send_msg('test2', callback_path="DeadBeef", query=query) + assert telegram._updater.bot.edit_message_text.call_count == 1 + assert log_has_re(r"TelegramError: .*", caplog) + + telegram._updater.bot.edit_message_text = MagicMock(side_effect=TelegramError("DeadBEEF")) + telegram._send_msg('test3', callback_path="DeadBeef", query=query) + + assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog) + def test__send_msg_network_error(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) From a7f8342171354ab361dc99554f2dbfae3f2f171d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Jun 2021 16:49:54 +0200 Subject: [PATCH 0678/1386] Add small documentation about reload disabling --- config_full.json.example | 4 +++- docs/telegram-usage.md | 2 ++ freqtrade/constants.py | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 6aeb756f3..bc9f33f96 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -178,7 +178,9 @@ "sell_fill": "on", "buy_cancel": "on", "sell_cancel": "on" - } + }, + "reload": true, + "balance_dust_level": 0.01 }, "api_server": { "enabled": false, diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 87ff38881..f5d9744b4 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -95,6 +95,7 @@ Example configuration showing the different settings: "buy_fill": "off", "sell_fill": "off" }, + "reload": true, "balance_dust_level": 0.01 }, ``` @@ -105,6 +106,7 @@ Example configuration showing the different settings: `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. ## Create a custom keyboard (command shortcut buttons) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 259aa0e03..013e9df41 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -275,7 +275,8 @@ CONF_SCHEMA = { 'default': 'off' }, } - } + }, + 'reload': {'type': 'boolean'}, }, 'required': ['enabled', 'token', 'chat_id'], }, From 96fbb226c5783fb9e50c37bc3a0f0593101ebb4c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Jun 2021 19:32:29 +0200 Subject: [PATCH 0679/1386] Implement better strategy checks part of #2696 --- freqtrade/strategy/interface.py | 19 +++++++++++++------ tests/strategy/test_interface.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b259a7977..65e27a2c2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -453,18 +453,25 @@ class IStrategy(ABC, HyperStrategyMixin): """ Ensure dataframe (length, last candle) was not modified, and has all elements we need. """ + message_template = "Dataframe returned from strategy has mismatching {}." message = "" - if df_len != len(dataframe): - message = "length" + if dataframe is None: + message = "No dataframe returned (return statement missing?)." + elif 'buy' not in dataframe: + message = "Buy column not set." + elif 'sell' not in dataframe: + message = "Sell column not set." + elif df_len != len(dataframe): + message = message_template.format("length") elif df_close != dataframe["close"].iloc[-1]: - message = "last close price" + message = message_template.format("last close price") elif df_date != dataframe["date"].iloc[-1]: - message = "last date" + message = message_template.format("last date") if message: if self.disable_dataframe_checks: - logger.warning(f"Dataframe returned from strategy has mismatching {message}.") + logger.warning(message) else: - raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.") + raise StrategyError(message) def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 64081fa37..04d12a51f 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -153,6 +153,8 @@ def test_assert_df_raise(mocker, caplog, ohlcv_history): def test_assert_df(ohlcv_history, caplog): df_len = len(ohlcv_history) - 1 + ohlcv_history.loc[:, 'buy'] = 0 + ohlcv_history.loc[:, 'sell'] = 0 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) @@ -170,6 +172,18 @@ def test_assert_df(ohlcv_history, caplog): match=r"Dataframe returned from strategy.*last date\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) + with pytest.raises(StrategyError, + match=r"No dataframe returned \(return statement missing\?\)."): + _STRATEGY.assert_df(None, len(ohlcv_history), + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) + with pytest.raises(StrategyError, + match="Buy column not set"): + _STRATEGY.assert_df(ohlcv_history.drop('buy', axis=1), len(ohlcv_history), + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) + with pytest.raises(StrategyError, + match="Sell column not set"): + _STRATEGY.assert_df(ohlcv_history.drop('sell', axis=1), len(ohlcv_history), + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) _STRATEGY.disable_dataframe_checks = True caplog.clear() From 122943d835e56a5aad6be3e1e172b351174e428d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Jun 2021 19:37:27 +0200 Subject: [PATCH 0680/1386] Don't run filter again for pairlist generator The generator implicitly runs filter - so it should not be ran again as that would void generator caching. closes #5103 --- freqtrade/plugins/pairlistmanager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index d1cdd2c5b..03f4760b8 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -83,7 +83,8 @@ class PairListManager(): pairlist = self._pairlist_handlers[0].gen_pairlist(tickers) # Process all Pairlist Handlers in the chain - for pairlist_handler in self._pairlist_handlers: + # except for the first one, which is the generator. + for pairlist_handler in self._pairlist_handlers[1:]: pairlist = pairlist_handler.filter_pairlist(pairlist, tickers) # Validation against blacklist happens after the chain of Pairlist Handlers From 347eceeda5b675474f0294a6db99bc660909995f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Jun 2021 20:30:40 +0200 Subject: [PATCH 0681/1386] Try fix fluky test --- tests/plugins/test_pairlist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 5e2274ce3..ae8f6e958 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -75,7 +75,7 @@ def whitelist_conf_agefilter(default_conf): "method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "refresh_period": 0, + "refresh_period": -1, }, { "method": "AgeFilter", @@ -687,7 +687,6 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - # freqtrade.config['exchange']['pair_whitelist'].append('HOT/BTC') previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() From 7f434c041389ec7edf2137db2f4172be8ad1d36b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 09:37:32 +0200 Subject: [PATCH 0682/1386] Simplify mkdocs jquery inclusion by using overrides instead of partials --- docs/overrides/main.html | 10 ++++++ docs/partials/header.html | 72 --------------------------------------- mkdocs.yml | 2 +- 3 files changed, 11 insertions(+), 73 deletions(-) create mode 100644 docs/overrides/main.html delete mode 100644 docs/partials/header.html diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000..5b116de4b --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block footer %} + {{ super() }} + + + + +{% endblock %} diff --git a/docs/partials/header.html b/docs/partials/header.html deleted file mode 100644 index 22132bc96..000000000 --- a/docs/partials/header.html +++ /dev/null @@ -1,72 +0,0 @@ -{#- -This file was automatically generated - do not edit --#} -{% set site_url = config.site_url | d(nav.homepage.url, true) | url %} -{% if not config.use_directory_urls and site_url[0] == site_url[-1] == "." %} -{% set site_url = site_url ~ "/index.html" %} -{% endif %} -
diff --git a/mkdocs.yml b/mkdocs.yml index cc5747225..e3e1ade86 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,7 +42,7 @@ theme: name: material logo: 'images/logo.png' favicon: 'images/logo.png' - custom_dir: 'docs' + custom_dir: 'docs/overrides' palette: primary: 'blue grey' accent: 'tear' From 97351c95c0e21ca906d54e9264b93d5b01be06f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 10:36:18 +0200 Subject: [PATCH 0683/1386] Add section about GPU support #5158 #5085 #3704 #2754 --- docs/faq.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index e5da550fd..d015ae50e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -136,6 +136,22 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us > type \path\to\mylogfile.log | findstr "something" ``` +### Why does freqtrade not have GPU support? + +First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations. +The GPU improvements would only apply to pandas-native calculations - or ones written by yourself. + +For hyperopt, freqtrade is using scikit-optimize, which is built on top of scikit-learn. +Their statement about GPU support is [pretty clear](https://scikit-learn.org/stable/faq.html#will-you-add-gpu-support). + +GPU's also are only good at crunching numbers (floating point operations). +For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting). +As such, GPU's are not too well suited for most parts of hyperopt. + +The benefit of using GPU would therefore be pretty slim - and will not justify the complexity introduced by trying to add GPU support. + +There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity). + ## Hyperopt module ### How many epochs do I need to get a good Hyperopt result? From 17f8936f420b83901d8b78c0f7a5a7099fa07637 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 03:00:59 +0000 Subject: [PATCH 0684/1386] Bump scipy from 1.6.3 to 1.7.0 Bumps [scipy](https://github.com/scipy/scipy) from 1.6.3 to 1.7.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.6.3...v1.7.0) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-minor ... 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 5e7e9d9d2..83e23e3ec 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.6.3 +scipy==1.7.0 scikit-learn==0.24.2 scikit-optimize==0.8.1 filelock==3.0.12 From fc7b372ce43e58aa46a5ab0cff6a71a278a69196 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 03:01:06 +0000 Subject: [PATCH 0685/1386] Bump ccxt from 1.51.40 to 1.51.77 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.51.40 to 1.51.77. - [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.51.40...1.51.77) --- 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 3d0b0256e..dab9861e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.3 pandas==1.2.4 -ccxt==1.51.40 +ccxt==1.51.77 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From eab6399490445bacc8a17fc97defa2a764fd0f8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 03:01:17 +0000 Subject: [PATCH 0686/1386] Bump prompt-toolkit from 3.0.18 to 3.0.19 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.18 to 3.0.19. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.18...3.0.19) --- updated-dependencies: - dependency-name: prompt-toolkit 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 3d0b0256e..7dee5cb01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,4 +40,4 @@ aiofiles==0.7.0 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.18 +prompt-toolkit==3.0.19 From a6628fc65f9e0b7d0be0dbec154a47d7eadc4264 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 03:01:21 +0000 Subject: [PATCH 0687/1386] Bump types-requests from 0.1.11 to 0.1.13 Bumps [types-requests](https://github.com/python/typeshed) from 0.1.11 to 0.1.13. - [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 924b35e1a..1867c543d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,5 +21,5 @@ nbconvert==6.0.7 # mypy types types-cachetools==0.1.7 types-filelock==0.1.3 -types-requests==0.1.11 +types-requests==0.1.13 types-tabulate==0.1.0 From bb0ee837bc5f03a59dcc09956cab8f91e9530633 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 03:01:22 +0000 Subject: [PATCH 0688/1386] Bump pycoingecko from 2.1.0 to 2.2.0 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/commits) --- updated-dependencies: - dependency-name: pycoingecko 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 3d0b0256e..b3419e437 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonschema==3.2.0 TA-Lib==0.4.20 technical==1.3.0 tabulate==0.8.9 -pycoingecko==2.1.0 +pycoingecko==2.2.0 jinja2==3.0.1 tables==3.6.1 blosc==1.10.4 From fdc04e27a4e6a71314dad58b0001bc947aa3b305 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 04:25:59 +0000 Subject: [PATCH 0689/1386] Bump types-tabulate from 0.1.0 to 0.1.1 Bumps [types-tabulate](https://github.com/python/typeshed) from 0.1.0 to 0.1.1. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-tabulate 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 1867c543d..927e1c813 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,4 +22,4 @@ nbconvert==6.0.7 types-cachetools==0.1.7 types-filelock==0.1.3 types-requests==0.1.13 -types-tabulate==0.1.0 +types-tabulate==0.1.1 From 2d05a8bea1d656f2d2f7c2261b6bd99537c8686b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 04:35:56 +0000 Subject: [PATCH 0690/1386] Bump types-cachetools from 0.1.7 to 0.1.8 Bumps [types-cachetools](https://github.com/python/typeshed) from 0.1.7 to 0.1.8. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools 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 1867c543d..bdac6d0f4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,7 @@ isort==5.8.0 nbconvert==6.0.7 # mypy types -types-cachetools==0.1.7 +types-cachetools==0.1.8 types-filelock==0.1.3 types-requests==0.1.13 types-tabulate==0.1.0 From 8c1484ed5e95ca0cef7af498e5870e886b85f6ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 06:06:08 +0000 Subject: [PATCH 0691/1386] Bump types-filelock from 0.1.3 to 0.1.4 Bumps [types-filelock](https://github.com/python/typeshed) from 0.1.3 to 0.1.4. - [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 328830454..30044058b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,6 +20,6 @@ nbconvert==6.0.7 # mypy types types-cachetools==0.1.8 -types-filelock==0.1.3 +types-filelock==0.1.4 types-requests==0.1.13 types-tabulate==0.1.1 From 0605cbb06eb26b4a879727447f2f76ea5fcd5232 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Tue, 22 Jun 2021 12:20:12 +0300 Subject: [PATCH 0692/1386] make "/profit N" command output be consistent with "/daily" and "/status table" commands --- freqtrade/rpc/rpc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2a7721af0..296793930 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -339,7 +339,10 @@ class RPC: self, stake_currency: str, fiat_display_currency: str, start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]: """ Returns cumulative profit statistics """ - trades = Trade.get_trades([Trade.open_date >= start_date]).order_by(Trade.id).all() + trade_filter = \ + (Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | \ + Trade.is_open.is_(True) + trades = Trade.get_trades(trade_filter).order_by(Trade.id).all() profit_all_coin = [] profit_all_ratio = [] From e97c82c51490b6c915da417c74abc1a6a484913b Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Tue, 22 Jun 2021 12:22:19 +0300 Subject: [PATCH 0693/1386] make "/profit N" command output be consistent with "/daily" and "/status table" commands --- freqtrade/rpc/rpc.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 296793930..b155de673 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -339,9 +339,8 @@ class RPC: self, stake_currency: str, fiat_display_currency: str, start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]: """ Returns cumulative profit statistics """ - trade_filter = \ - (Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | \ - Trade.is_open.is_(True) + trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | + Trade.is_open.is_(True)) trades = Trade.get_trades(trade_filter).order_by(Trade.id).all() profit_all_coin = [] From 10e94350e9c9052af7ba869eb9812a1ca72a03e4 Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Tue, 22 Jun 2021 14:59:43 +0200 Subject: [PATCH 0694/1386] Update installation.md --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 25994fdc0..5c6ac001f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces sudo apt-get update # install packages - sudo apt install -y python3-pip python3.7-venv python3.7-dev python3-pandas git + sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git ``` === "RaspberryPi/Raspbian" From 3c70768e18dd43d6ac96790c36e7f9382d53071a Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Wed, 23 Jun 2021 07:30:08 +0300 Subject: [PATCH 0695/1386] make "/profit N" command output be consistent with "/daily" and "/status table" commands --- 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 6cb48aef1..16c9fddcc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -482,7 +482,7 @@ class Telegram(RPCHandler): timescale = None try: if context.args: - timescale = int(context.args[0]) + timescale = int(context.args[0]) - 1 today_start = datetime.combine(date.today(), datetime.min.time()) start_date = today_start - timedelta(days=timescale) except (TypeError, ValueError, IndexError): From f7c09ba63a146bd5a24e138d1de9e19da69ffd39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Jun 2021 18:17:40 +0200 Subject: [PATCH 0696/1386] Log endpoint should use static rpc class --- freqtrade/rpc/api_server/api_v1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index e907b92f0..965664028 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -162,8 +162,8 @@ def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)): @router.get('/logs', response_model=Logs, tags=['info']) -def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_get_logs(limit) +def logs(limit: Optional[int] = None): + return RPC._rpc_get_logs(limit) @router.post('/start', response_model=StatusMsg, tags=['botcontrol']) From c938edc01bada0e5fbbb3d1641615eb9ea848b06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Jun 2021 18:18:01 +0200 Subject: [PATCH 0697/1386] Apply dataprovider to /pair_history endpoint --- freqtrade/rpc/rpc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b155de673..506df4cca 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -825,7 +825,10 @@ class RPC: if pair not in _data: raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.") from freqtrade.resolvers.strategy_resolver import StrategyResolver + from freqtrade.data.dataprovider import DataProvider strategy = StrategyResolver.load_strategy(config) + strategy.dp = DataProvider(config, exchange=None, pairlists=None) + df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, From e0d3ca6c6d7a24a0f7ba3e4beaacf0203a22001b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Jun 2021 18:44:59 +0200 Subject: [PATCH 0698/1386] Fix import sorting --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 506df4cca..8f806f555 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -824,8 +824,8 @@ class RPC: ) if pair not in _data: raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.") - from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.data.dataprovider import DataProvider + from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(config) strategy.dp = DataProvider(config, exchange=None, pairlists=None) From 538a1acdb5b2ac5c20a57edbc10fb71f0255fd8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Jun 2021 22:53:27 +0200 Subject: [PATCH 0699/1386] Add Binance Broker ad to documentation page --- docs/overrides/main.html | 58 +++++++++++++++++++++++++++++++++++ docs/stylesheets/ft.extra.css | 11 +++++++ 2 files changed, 69 insertions(+) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 5b116de4b..e3d1a5d4a 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,5 +1,41 @@ {% extends "base.html" %} + + +{% block site_nav %} + + + {% if nav %} + {% if page and page.meta and page.meta.hide %} + {% set hidden = "hidden" if "navigation" in page.meta.hide %} + {% endif %} + + {% endif %} + + + {% if page.toc and not "toc.integrate" in features %} + {% if page and page.meta and page.meta.hide %} + {% set hidden = "hidden" if "toc" in page.meta.hide %} + {% endif %} + + {% endif %} +{% endblock %} + {% block footer %} {{ super() }} @@ -7,4 +43,26 @@ + + // Load binance SDK + + + {% endblock %} diff --git a/docs/stylesheets/ft.extra.css b/docs/stylesheets/ft.extra.css index 3369fa177..f7e1f48d8 100644 --- a/docs/stylesheets/ft.extra.css +++ b/docs/stylesheets/ft.extra.css @@ -11,3 +11,14 @@ .rst-versions .rst-other-versions { color: white; } + + +#widget-wrapper { + height: calc(220px * 0.5625 + 18px); + width: 220px; + margin: 0 auto 16px auto; + border-style: solid; + border-color: var(--md-code-bg-color); + border-width: 1px; + border-radius: 5px; +} From f585ffa264e1a44e193b6e5e43bb30f4e55c70c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Jun 2021 22:53:46 +0200 Subject: [PATCH 0700/1386] Add Dark theme to Documentation --- mkdocs.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index e3e1ade86..854939ca0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,8 +44,18 @@ theme: favicon: 'images/logo.png' custom_dir: 'docs/overrides' palette: - primary: 'blue grey' - accent: 'tear' + - scheme: default + primary: 'blue grey' + accent: 'tear' + toggle: + icon: material/toggle-switch-off-outline + name: Switch to dark mode + - scheme: slate + primary: 'blue grey' + accent: 'tear' + toggle: + icon: material/toggle-switch-off-outline + name: Switch to dark mode extra_css: - 'stylesheets/ft.extra.css' extra_javascript: From 2ade3ec7b9b788bdbfd069a8127961388a07c476 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Jun 2021 23:09:06 +0200 Subject: [PATCH 0701/1386] Add max-width query to hide on small screens --- docs/stylesheets/ft.extra.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/stylesheets/ft.extra.css b/docs/stylesheets/ft.extra.css index f7e1f48d8..8e6f9929a 100644 --- a/docs/stylesheets/ft.extra.css +++ b/docs/stylesheets/ft.extra.css @@ -22,3 +22,7 @@ border-width: 1px; border-radius: 5px; } + +@media screen and (max-width: 700px) { + #widget-wrapper { display: none; } +} From 9e91240283cc2499683574e60853a21ab0e1de69 Mon Sep 17 00:00:00 2001 From: Carlo Revelli Date: Fri, 25 Jun 2021 10:43:40 +0100 Subject: [PATCH 0702/1386] binance-portal --- docs/overrides/main.html | 2 +- docs/stylesheets/ft.extra.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index e3d1a5d4a..b2138dd7b 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -58,7 +58,7 @@ window.binanceBrokerPortalSdk.initBrokerSDK('#widget', { apiHost: 'https://www.binance.com', brokerId: 'R4BD3S82', - site: "site" + slideTime: 4e4, }); } catch(err) { console.log(err) diff --git a/docs/stylesheets/ft.extra.css b/docs/stylesheets/ft.extra.css index 8e6f9929a..930f2038a 100644 --- a/docs/stylesheets/ft.extra.css +++ b/docs/stylesheets/ft.extra.css @@ -23,6 +23,6 @@ border-radius: 5px; } -@media screen and (max-width: 700px) { +@media screen and (max-width: calc(76.25em + 1px)) { #widget-wrapper { display: none; } } From 69a3aee01e3d4174693977bdb7103b8234969bca Mon Sep 17 00:00:00 2001 From: Carlo Revelli Date: Fri, 25 Jun 2021 10:53:52 +0100 Subject: [PATCH 0703/1386] minor edits --- docs/overrides/main.html | 2 +- docs/stylesheets/ft.extra.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index b2138dd7b..dfc5264be 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -44,7 +44,7 @@ - // Load binance SDK +
- - - - -

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 z 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 literal 0 HcmV?d00001 diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 5747db335..d2a27dd26 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -1,16 +1,15 @@ # Downloads don't work automatically, since the URL is regenerated via javascript. # Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib -# Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" python -m pip install --upgrade pip $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" if ($pyv -eq '3.7') { - pip install build_helpers\TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl } if ($pyv -eq '3.8') { - pip install build_helpers\TA_Lib-0.4.19-cp38-cp38-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.20-cp38-cp38-win_amd64.whl } pip install -r requirements-dev.txt diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 168938973..edc0a1404 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -1,3 +1,5 @@ +# Windows installation + We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure). If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. @@ -21,7 +23,7 @@ git clone https://github.com/freqtrade/freqtrade.git Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). -As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.19‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version) +As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.20‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version). Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows. Other versions must be downloaded from the above link. From c0d3a31ddb7ac11d1e96cb3a2ba03ecc53cd7dcf Mon Sep 17 00:00:00 2001 From: Nicolas Menescardi Date: Mon, 24 May 2021 11:08:17 -0300 Subject: [PATCH 0538/1386] Update strategy-advanced.md fix some typos --- docs/strategy-advanced.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index cb759eb2f..3436604a9 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -331,7 +331,7 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. -However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if a order did time out or not. +However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not. !!! Note Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. @@ -557,7 +557,7 @@ Both attributes and methods may be overridden, altering behavior of the original ## Embedding Strategies -Freqtrade provides you with with an easy way to embed the strategy into your configuration file. +Freqtrade provides you with an easy way to embed the strategy into your configuration file. This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field, in your chosen config file. From 9465fd390a36f3e1425e717506c3aed9f4302221 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 May 2021 17:01:53 +0200 Subject: [PATCH 0539/1386] Fix devcontainer --- .devcontainer/Dockerfile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 19e09c969..43d54e647 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,20 +1,21 @@ FROM freqtradeorg/freqtrade:develop +USER root # Install dependencies COPY requirements-dev.txt /freqtrade/ + RUN apt-get update \ - && apt-get -y install git mercurial sudo vim \ + && apt-get -y install git mercurial sudo vim build-essential \ && apt-get clean \ - && pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \ - && useradd -u 1000 -U -m ftuser \ && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ - && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ - && mv /root/.local /home/ftuser/.local/ \ + && echo "export HISTFILE=~/commandhistory/.bash_hi>story" >> /home/ftuser/.bashrc \ && chown ftuser:ftuser -R /home/ftuser/.local/ \ && chown ftuser: -R /home/ftuser/ USER ftuser +RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir + # Empty the ENTRYPOINT to allow all commands ENTRYPOINT [] From bd44deea0dbfbcf3a651d1533f04b019ec5291f5 Mon Sep 17 00:00:00 2001 From: Rikj000 Date: Mon, 24 May 2021 18:51:33 +0200 Subject: [PATCH 0540/1386] BugFix - hyperopt-show --print-json include non-optimized params --- freqtrade/optimize/hyperopt_tools.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) mode change 100644 => 100755 freqtrade/optimize/hyperopt_tools.py diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py old mode 100644 new mode 100755 index 49e70913f..8fa03a0d2 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -93,7 +93,7 @@ class HyperoptTools(): if print_json: result_dict: Dict = {} for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: - HyperoptTools._params_update_for_json(result_dict, params, s) + HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s) print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: @@ -106,11 +106,20 @@ class HyperoptTools(): HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") @staticmethod - def _params_update_for_json(result_dict, params, space: str) -> None: + def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None: if space in params: space_params = HyperoptTools._space_params(params, space) + space_non_optimized = HyperoptTools._space_params(non_optimized, space) + all_space_params = space_params + + # Include non optimized params if there are any + if len(space_non_optimized) > 0: + for non_optimized_param in space_non_optimized: + if non_optimized_param not in all_space_params: + all_space_params[non_optimized_param] = space_non_optimized[non_optimized_param] + if space in ['buy', 'sell']: - result_dict.setdefault('params', {}).update(space_params) + result_dict.setdefault('params', {}).update(all_space_params) elif space == 'roi': # TODO: get rid of OrderedDict when support for python 3.6 will be # dropped (dicts keep the order as the language feature) @@ -120,10 +129,10 @@ class HyperoptTools(): # OrderedDict is used to keep the numeric order of the items # in the dict. result_dict['minimal_roi'] = OrderedDict( - (str(k), v) for k, v in space_params.items() + (str(k), v) for k, v in all_space_params.items() ) else: # 'stoploss', 'trailing' - result_dict.update(space_params) + result_dict.update(all_space_params) @staticmethod def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None: From 7dcf94f80cd9ce58d71bba0f515fb5f9f3b47177 Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Tue, 25 May 2021 08:18:14 +0700 Subject: [PATCH 0541/1386] Update Dockerfile --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 43d54e647..6389a50df 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update \ && apt-get clean \ && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ - && echo "export HISTFILE=~/commandhistory/.bash_hi>story" >> /home/ftuser/.bashrc \ + && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ && chown ftuser:ftuser -R /home/ftuser/.local/ \ && chown ftuser: -R /home/ftuser/ From a747312c1ebaabb71e9fd2eeff85fc7c96cfb45f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 May 2021 18:02:07 +0200 Subject: [PATCH 0542/1386] Explicitly provide is_open to trade Object closes #5015 --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1c3a759f4..56596f2b9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -601,6 +601,7 @@ class FreqtradeBot(LoggingMixin): pair=pair, stake_amount=stake_amount, amount=amount, + is_open=True, amount_requested=amount_requested, fee_open=fee, fee_close=fee, From cc5769e900514aa3180feb3db90d6362996e91bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 May 2021 19:24:56 +0200 Subject: [PATCH 0543/1386] Convert np.int64 to proper int closes #5018 --- freqtrade/optimize/hyperopt.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 85bcbb8e3..430fe290a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -11,6 +11,7 @@ from datetime import datetime, timezone from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional +import numpy as np import progressbar import rapidjson @@ -162,8 +163,13 @@ class Hyperopt: While not a valid json object - this allows appending easily. :param epoch: result dictionary for this epoch. """ + def default_parser(x): + if isinstance(x, np.integer): + return int(x) + return str(x) + with self.results_file.open('a') as f: - rapidjson.dump(epoch, f, default=str, + rapidjson.dump(epoch, f, default=default_parser, number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN) f.write("\n") From 8e89d3e6e4aace3f8f688f19ce47b254338626de Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 May 2021 19:33:34 +0200 Subject: [PATCH 0544/1386] Fix sort error --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 430fe290a..49273d3de 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -11,8 +11,8 @@ from datetime import datetime, timezone from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional -import numpy as np +import numpy as np import progressbar import rapidjson from colorama import Fore, Style From 42453333bef8f600c29007e69bd5be508953a9a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 07:38:47 +0200 Subject: [PATCH 0545/1386] Align coinbase download with ccxt limits Align with https://github.com/ccxt/ccxt/issues/9268 --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bybit.py | 1 - freqtrade/exchange/coinbasepro.py | 23 +++++++++++++++++++++++ freqtrade/exchange/hitbtc.py | 1 - requirements.txt | 2 +- 5 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 freqtrade/exchange/coinbasepro.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 23ba2eb10..015e0c869 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -7,6 +7,7 @@ from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit +from freqtrade.exchange.coinbasepro import Coinbasepro from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 4a44bb42d..163f8c44e 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -18,7 +18,6 @@ class Bybit(Exchange): may still not work as expected. """ - # fetchCurrencies API point requires authentication for Bybit, _ft_has: Dict = { "ohlcv_candle_limit": 200, } diff --git a/freqtrade/exchange/coinbasepro.py b/freqtrade/exchange/coinbasepro.py new file mode 100644 index 000000000..7dd9c80dc --- /dev/null +++ b/freqtrade/exchange/coinbasepro.py @@ -0,0 +1,23 @@ +""" CoinbasePro exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Coinbasepro(Exchange): + """ + CoinbasePro exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 300, + } diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py index 763535263..a48c9a198 100644 --- a/freqtrade/exchange/hitbtc.py +++ b/freqtrade/exchange/hitbtc.py @@ -17,7 +17,6 @@ class Hitbtc(Exchange): may still not work as expected. """ - # fetchCurrencies API point requires authentication for Hitbtc, _ft_has: Dict = { "ohlcv_candle_limit": 1000, "ohlcv_params": {"sort": "DESC"} diff --git a/requirements.txt b/requirements.txt index df611aedf..512b2a588 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.3 pandas==1.2.4 -ccxt==1.50.30 +ccxt==1.50.48 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From f3d8e5c9e418d6645eb07a7bbd2e84840d656bb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 10:44:35 +0200 Subject: [PATCH 0546/1386] Improve hyperopt docs closes #4949 --- docs/hyperopt.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index d8f4a8071..0d7b1cc32 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -249,15 +249,16 @@ We continue to define hyperoptable parameters: ```python class MyAwesomeStrategy(IStrategy): - buy_adx = IntParameter(20, 40, default=30, space="buy") + buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy") buy_rsi = IntParameter(20, 40, default=30, space="buy") - buy_adx_enabled = CategoricalParameter([True, False], space="buy") - buy_rsi_enabled = CategoricalParameter([True, False], space="buy") - buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal'], space="buy") + buy_adx_enabled = CategoricalParameter([True, False], default=True, space="buy") + buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy") + buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy") ``` -Above definition says: I have five parameters I want to randomly combine to find the best combination. -Two of them are integer values (`buy_adx` and `buy_rsi`) and I want you test in the range of values 20 to 40. +The above definition says: I have five parameters I want to randomly combine to find the best combination. +`buy_rsi` is an integer parameter, which will be tested between 20 and 40. This space has a size of 20. +`buy_adx` is a decimal parameter, which will be evaluated between 20 and 40 with 1 decimal place (so values are 20.1, 20.2, ...). This space has a size of 200. Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. From 0c9b913cad25e0f75cde562415014116e28c3153 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 11:10:10 +0200 Subject: [PATCH 0547/1386] Version bump 2021.5 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 68bcad396..ed0c70417 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.4' +__version__ = '2021.5' if __version__ == 'develop': From a6cd35365591d5ec6d7d71cec262c051470b8c03 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 11:22:22 +0200 Subject: [PATCH 0548/1386] Address random ci failure --- tests/rpc/test_rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index b005fb105..904a7e74f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -422,7 +422,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' assert stats['latest_trade_date'] == 'just now' - assert stats['avg_duration'] == '0:00:00' + assert stats['avg_duration'] in ('0:00:00', '0:00:01') assert stats['best_pair'] == 'ETH/BTC' assert prec_satoshi(stats['best_rate'], 6.2) @@ -433,7 +433,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' assert stats['latest_trade_date'] == 'just now' - assert stats['avg_duration'] == '0:00:00' + assert stats['avg_duration'] in ('0:00:00', '0:00:01') assert stats['best_pair'] == 'ETH/BTC' assert prec_satoshi(stats['best_rate'], 6.2) assert isnan(stats['profit_all_coin']) From 8bef7217ec0af25e08b91d8cdc3829edfffd9183 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 11:24:01 +0200 Subject: [PATCH 0549/1386] Forgot to save :O --- tests/rpc/test_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 904a7e74f..e7a968e37 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -433,7 +433,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, 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') + assert stats['avg_duration'] in ('0:00:00', '0:00:01') assert stats['best_pair'] == 'ETH/BTC' assert prec_satoshi(stats['best_rate'], 6.2) assert isnan(stats['profit_all_coin']) From c5c323ca8895d45567d4d0e5a7379a6c73a51606 Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 27 May 2021 16:35:27 +0700 Subject: [PATCH 0550/1386] Settings notify sell in telegram base on sell reason (#5028) * BREAK: notification sell by sell reason * Update constants.py * Update telegram.py * Update telegram-usage.md * Update telegram.py * Update telegram.py * Fix test fail * Update config_full.json.example * Update telegram-usage.md * Update telegram.py * Update telegram.py * Update telegram-usage.md * validate value of sell object * Fix linter * Update constants.py * Make telegram sample slightly more positive Co-authored-by: Matthias --- config_full.json.example | 11 ++++++++++- docs/telegram-usage.md | 41 ++++++++++++++++++++++++--------------- freqtrade/constants.py | 8 +++++++- freqtrade/rpc/telegram.py | 40 +++++++++++++++++++++++++------------- 4 files changed, 69 insertions(+), 31 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 24d364fdf..6aeb756f3 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -165,7 +165,16 @@ "startup": "on", "buy": "on", "buy_fill": "on", - "sell": "on", + "sell": { + "roi": "off", + "emergency_sell": "off", + "force_sell": "off", + "sell_signal": "off", + "trailing_stop_loss": "off", + "stop_loss": "off", + "stoploss_on_exchange": "off", + "custom_sell": "off" + }, "sell_fill": "on", "buy_cancel": "on", "sell_cancel": "on" diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 6174bf0fe..991b5f1fb 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -72,22 +72,31 @@ Example configuration showing the different settings: ``` json "telegram": { - "enabled": true, - "token": "your_telegram_token", - "chat_id": "your_telegram_chat_id", - "notification_settings": { - "status": "silent", - "warning": "on", - "startup": "off", - "buy": "silent", - "sell": "on", - "buy_cancel": "silent", - "sell_cancel": "on", - "buy_fill": "off", - "sell_fill": "off" - }, - "balance_dust_level": 0.01 - }, + "enabled": true, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id", + "notification_settings": { + "status": "silent", + "warning": "on", + "startup": "off", + "buy": "silent", + "sell": { + "roi": "silent", + "emergency_sell": "on", + "force_sell": "on", + "sell_signal": "silent", + "trailing_stop_loss": "on", + "stop_loss": "on", + "stoploss_on_exchange": "on", + "custom_sell": "silent" + }, + "buy_cancel": "silent", + "sell_cancel": "on", + "buy_fill": "off", + "sell_fill": "off" + }, + "balance_dust_level": 0.01 +}, ``` `buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 5ec60eb59..2e0efc8e7 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -260,7 +260,13 @@ CONF_SCHEMA = { 'enum': TELEGRAM_SETTING_OPTIONS, 'default': 'off' }, - 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell': { + 'type': 'object', + 'additionalProperties': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS + } + }, 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'sell_fill': { 'type': 'string', diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b9e90dc8d..cca87ad91 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -233,44 +233,58 @@ class Telegram(RPCHandler): def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ - noti = self._config['telegram'].get('notification_settings', {} - ).get(str(msg['type']), 'on') + default_noti = 'on' + + msg_type = msg['type'] + noti = '' + if msg_type == RPCMessageType.SELL: + sell_noti = self._config['telegram'] \ + .get('notification_settings', {}).get(str(msg_type), {}) + # For backward compatibility sell still be string + if isinstance(noti, str): + noti = sell_noti + else: + noti = sell_noti.get(str(msg['sell_reason']), default_noti) + else: + noti = self._config['telegram'] \ + .get('notification_settings', {}).get(str(msg_type), default_noti) + if noti == 'off': - logger.info(f"Notification '{msg['type']}' not sent.") + logger.info(f"Notification '{msg_type}' not sent.") # Notification disabled return - if msg['type'] == RPCMessageType.BUY: + if msg_type == RPCMessageType.BUY: message = self._format_buy_msg(msg) - elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): - msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell' + elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): + msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell' message = ("\N{WARNING SIGN} *{exchange}:* " "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == RPCMessageType.BUY_FILL: + elif msg_type == RPCMessageType.BUY_FILL: message = ("\N{LARGE CIRCLE} *{exchange}:* " "Buy order for {pair} (#{trade_id}) filled " "for {open_rate}.".format(**msg)) - elif msg['type'] == RPCMessageType.SELL_FILL: + elif msg_type == RPCMessageType.SELL_FILL: message = ("\N{LARGE CIRCLE} *{exchange}:* " "Sell order for {pair} (#{trade_id}) filled " "for {close_rate}.".format(**msg)) - elif msg['type'] == RPCMessageType.SELL: + elif msg_type == RPCMessageType.SELL: message = self._format_sell_msg(msg) - elif msg['type'] == RPCMessageType.STATUS: + elif msg_type == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.WARNING: + elif msg_type == RPCMessageType.WARNING: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.STARTUP: + elif msg_type == RPCMessageType.STARTUP: message = '{status}'.format(**msg) else: - raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) + raise NotImplementedError('Unknown message type: {}'.format(msg_type)) self._send_msg(message, disable_notification=(noti == 'silent')) From 2f79958acb3c9a120d6a82d04676266be1c12c6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 14:43:43 +0200 Subject: [PATCH 0551/1386] Move declarative_base import to import from .orm --- freqtrade/persistence/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f2e7a10c4..6733b66a6 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -9,8 +9,7 @@ from typing import Any, Dict, List, Optional from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Query, relationship +from sqlalchemy.orm import Query, relationship, declarative_base from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool From c31cb6711874b99f175c1508475d75ad69da4c5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 14:53:08 +0200 Subject: [PATCH 0552/1386] Further changes for sqlalchemy 1.4 --- freqtrade/persistence/migrations.py | 21 ++++++++++++--------- freqtrade/persistence/models.py | 2 +- tests/test_persistence.py | 29 ++++++++++++++++------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index d89256baf..bb6860373 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -1,7 +1,7 @@ import logging from typing import List -from sqlalchemy import inspect +from sqlalchemy import inspect, text logger = logging.getLogger(__name__) @@ -62,15 +62,17 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col amount_requested = get_column_def(cols, 'amount_requested', 'amount') # Schema migration necessary - engine.execute(f"alter table trades rename to {table_back_name}") - # drop indexes on backup table - for index in inspector.get_indexes(table_back_name): - engine.execute(f"drop index {index['name']}") + with engine.begin() as connection: + connection.execute(text(f"alter table trades rename to {table_back_name}")) + # drop indexes on backup table + for index in inspector.get_indexes(table_back_name): + connection.execute(text(f"drop index {index['name']}")) # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) # Copy data back - following the correct schema - engine.execute(f"""insert into trades + with engine.begin() as connection: + connection.execute(text(f"""insert into trades (id, exchange, pair, is_open, fee_open, fee_open_cost, fee_open_currency, fee_close, fee_close_cost, fee_open_currency, open_rate, @@ -104,11 +106,12 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} - """) + """)) def migrate_open_orders_to_trades(engine): - engine.execute(""" + with engine.begin() as connection: + connection.execute(text(""" insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open) select id ft_trade_id, pair ft_pair, open_order_id, case when close_rate_requested is null then 'buy' @@ -120,7 +123,7 @@ def migrate_open_orders_to_trades(engine): 'stoploss' ft_order_side, 1 ft_is_open from trades where stoploss_order_id is not null - """) + """)) def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List): diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 6733b66a6..6754623a8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError -from sqlalchemy.orm import Query, relationship, declarative_base +from sqlalchemy.orm import Query, declarative_base, relationship from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 669f220bb..1814ddbbe 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock import arrow import pytest -from sqlalchemy import create_engine, inspect +from sqlalchemy import create_engine, inspect, text from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException @@ -486,9 +486,10 @@ def test_migrate_old(mocker, default_conf, fee): mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine) # Create table using the old format - engine.execute(create_table_old) - engine.execute(insert_table_old) - engine.execute(insert_table_old2) + with engine.begin() as connection: + connection.execute(text(create_table_old)) + connection.execute(text(insert_table_old)) + connection.execute(text(insert_table_old2)) # Run init to test migration init_db(default_conf['db_url'], default_conf['dry_run']) @@ -579,15 +580,16 @@ def test_migrate_new(mocker, default_conf, fee, caplog): mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine) # Create table using the old format - engine.execute(create_table_old) - engine.execute("create index ix_trades_is_open on trades(is_open)") - engine.execute("create index ix_trades_pair on trades(pair)") - engine.execute(insert_table_old) + with engine.begin() as connection: + connection.execute(text(create_table_old)) + connection.execute(text("create index ix_trades_is_open on trades(is_open)")) + connection.execute(text("create index ix_trades_pair on trades(pair)")) + connection.execute(text(insert_table_old)) - # fake previous backup - engine.execute("create table trades_bak as select * from trades") + # fake previous backup + connection.execute(text("create table trades_bak as select * from trades")) - engine.execute("create table trades_bak1 as select * from trades") + connection.execute(text("create table trades_bak1 as select * from trades")) # Run init to test migration init_db(default_conf['db_url'], default_conf['dry_run']) @@ -722,8 +724,9 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine) # Create table using the old format - engine.execute(create_table_old) - engine.execute(insert_table_old) + with engine.begin() as connection: + connection.execute(text(create_table_old)) + connection.execute(text(insert_table_old)) # Run init to test migration init_db(default_conf['db_url'], default_conf['dry_run']) From eaa47ff335a173d8cd01a5ec4740b470b4bf26d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 17:34:20 +0200 Subject: [PATCH 0553/1386] Don't use autocommit --- freqtrade/persistence/models.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 6754623a8..816e20adb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -9,9 +9,7 @@ from typing import Any, Dict, List, Optional from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError -from sqlalchemy.orm import Query, declarative_base, relationship -from sqlalchemy.orm.scoping import scoped_session -from sqlalchemy.orm.session import sessionmaker +from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint @@ -49,7 +47,7 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: }) try: - engine = create_engine(db_url, **kwargs) + engine = create_engine(db_url, future=True, **kwargs) except NoSuchModuleError: raise OperationalException(f"Given value for db_url: '{db_url}' " f"is no valid database URL! (See {_SQL_DOCS_URL})") @@ -57,7 +55,7 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version - Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) + Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True)) Trade.query = Trade._session.query_property() Order.query = Trade._session.query_property() PairLock.query = Trade._session.query_property() From 6fb32c35949e012ca00231e249e3c79c6fb01c07 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 19:52:33 +0200 Subject: [PATCH 0554/1386] Use commit instead of .flush() --- freqtrade/freqtradebot.py | 9 ++++++--- freqtrade/persistence/models.py | 6 ++++-- freqtrade/persistence/pairlock_middleware.py | 4 ++-- freqtrade/rpc/rpc.py | 7 +++---- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 56596f2b9..c7d27b98c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -187,7 +187,7 @@ class FreqtradeBot(LoggingMixin): if self.get_free_open_trades(): self.enter_positions() - Trade.query.session.flush() + Trade.query.session.commit() def process_stopped(self) -> None: """ @@ -620,7 +620,7 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(trade, order_id, order) Trade.query.session.add(trade) - Trade.query.session.flush() + Trade.query.session.commit() # Updating wallets self.wallets.update() @@ -706,6 +706,7 @@ class FreqtradeBot(LoggingMixin): if (self.strategy.order_types.get('stoploss_on_exchange') and self.handle_stoploss_on_exchange(trade)): trades_closed += 1 + Trade.query.session.commit() continue # Check if we can sell our current pair if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): @@ -1036,6 +1037,7 @@ class FreqtradeBot(LoggingMixin): elif order['side'] == 'sell': self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + Trade.query.session.commit() def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool: """ @@ -1233,7 +1235,7 @@ class FreqtradeBot(LoggingMixin): # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) - Trade.query.session.flush() + Trade.query.session.commit() # Lock pair for one candle to prevent immediate re-buys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), @@ -1374,6 +1376,7 @@ class FreqtradeBot(LoggingMixin): # Handling of this will happen in check_handle_timeout. return True trade.update(order) + Trade.query.session.commit() # Updating wallets when order is closed if not trade.is_open: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 816e20adb..eaade8869 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -74,7 +74,7 @@ def cleanup_db() -> None: Flushes all pending operations to disk. :return: None """ - Trade.query.session.flush() + Trade.query.session.commit() def clean_dry_run_db() -> None: @@ -86,6 +86,7 @@ def clean_dry_run_db() -> None: # Check we are updating only a dry_run order not a prod one if 'dry_run' in trade.open_order_id: trade.open_order_id = None + Trade.query.session.commit() class Order(_DECL_BASE): @@ -174,6 +175,7 @@ class Order(_DECL_BASE): if filtered_orders: oobj = filtered_orders[0] oobj.update_from_ccxt_object(order) + Order.query.session.commit() else: logger.warning(f"Did not find order for {order}.") @@ -709,7 +711,7 @@ class Trade(_DECL_BASE, LocalTrade): Order.query.session.delete(order) Trade.query.session.delete(self) - Trade.query.session.flush() + Trade.query.session.commit() @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 245f7cdab..af904f693 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -49,7 +49,7 @@ class PairLocks(): ) if PairLocks.use_db: PairLock.query.session.add(lock) - PairLock.query.session.flush() + PairLock.query.session.commit() else: PairLocks.locks.append(lock) @@ -99,7 +99,7 @@ class PairLocks(): for lock in locks: lock.active = False if PairLocks.use_db: - PairLock.query.session.flush() + PairLock.query.session.commit() @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3f26619a9..d0755992b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -569,7 +569,7 @@ class RPC: # Execute sell for all open orders for trade in Trade.get_open_trades(): _exec_forcesell(trade) - Trade.query.session.flush() + Trade.query.session.commit() self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} @@ -582,7 +582,7 @@ class RPC: raise RPCException('invalid argument') _exec_forcesell(trade) - Trade.query.session.flush() + Trade.query.session.commit() self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} @@ -705,8 +705,7 @@ class RPC: lock.active = False lock.lock_end_time = datetime.now(timezone.utc) - # session is always the same - PairLock.query.session.flush() + PairLock.query.session.commit() return self._rpc_locks() From a01d05997e4239f0c21bcdfaaa8a24e2a7697c42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 07:57:52 +0200 Subject: [PATCH 0555/1386] Add Trade.commit method for easy use --- freqtrade/freqtradebot.py | 12 ++++++------ freqtrade/persistence/models.py | 8 ++++++-- freqtrade/rpc/rpc.py | 4 ++-- tests/test_persistence.py | 1 + 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c7d27b98c..2e3240cfe 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -187,7 +187,7 @@ class FreqtradeBot(LoggingMixin): if self.get_free_open_trades(): self.enter_positions() - Trade.query.session.commit() + Trade.commit() def process_stopped(self) -> None: """ @@ -620,7 +620,7 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(trade, order_id, order) Trade.query.session.add(trade) - Trade.query.session.commit() + Trade.commit() # Updating wallets self.wallets.update() @@ -706,7 +706,7 @@ class FreqtradeBot(LoggingMixin): if (self.strategy.order_types.get('stoploss_on_exchange') and self.handle_stoploss_on_exchange(trade)): trades_closed += 1 - Trade.query.session.commit() + Trade.commit() continue # Check if we can sell our current pair if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): @@ -1037,7 +1037,7 @@ class FreqtradeBot(LoggingMixin): elif order['side'] == 'sell': self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - Trade.query.session.commit() + Trade.commit() def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool: """ @@ -1235,7 +1235,7 @@ class FreqtradeBot(LoggingMixin): # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) - Trade.query.session.commit() + Trade.commit() # Lock pair for one candle to prevent immediate re-buys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), @@ -1376,7 +1376,7 @@ class FreqtradeBot(LoggingMixin): # Handling of this will happen in check_handle_timeout. return True trade.update(order) - Trade.query.session.commit() + Trade.commit() # Updating wallets when order is closed if not trade.is_open: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index eaade8869..5648c1aee 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -74,7 +74,7 @@ def cleanup_db() -> None: Flushes all pending operations to disk. :return: None """ - Trade.query.session.commit() + Trade.commit() def clean_dry_run_db() -> None: @@ -86,7 +86,7 @@ def clean_dry_run_db() -> None: # Check we are updating only a dry_run order not a prod one if 'dry_run' in trade.open_order_id: trade.open_order_id = None - Trade.query.session.commit() + Trade.commit() class Order(_DECL_BASE): @@ -711,6 +711,10 @@ class Trade(_DECL_BASE, LocalTrade): Order.query.session.delete(order) Trade.query.session.delete(self) + Trade.commit() + + @staticmethod + def commit(): Trade.query.session.commit() @staticmethod diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d0755992b..b55a1660a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -569,7 +569,7 @@ class RPC: # Execute sell for all open orders for trade in Trade.get_open_trades(): _exec_forcesell(trade) - Trade.query.session.commit() + Trade.commit() self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} @@ -582,7 +582,7 @@ class RPC: raise RPCException('invalid argument') _exec_forcesell(trade) - Trade.query.session.commit() + Trade.commit() self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1814ddbbe..6e4030b09 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1291,6 +1291,7 @@ def test_Trade_object_idem(): excludes = ( 'delete', 'session', + 'commit', 'query', 'open_date', 'get_best_pair', From 17f74f7da8f89b0e784e9c1e81001b9374a52ebb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 14:11:02 +0200 Subject: [PATCH 0556/1386] Ensure commit happens on forcebuy --- freqtrade/rpc/rpc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b55a1660a..c609bccb8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -615,6 +615,7 @@ class RPC: # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): + Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade else: From b82f7a2dfdeb660d1156e90b80aad68f2a8c6c5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 08:34:58 +0200 Subject: [PATCH 0557/1386] Update orders-migrations to work with new sqlalchemy syntax --- freqtrade/persistence/migrations.py | 30 +++++------ tests/test_persistence.py | 78 +++++++++++++++-------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index bb6860373..00c9b91eb 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -128,23 +128,25 @@ def migrate_open_orders_to_trades(engine): def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List): # Schema migration necessary - engine.execute(f"alter table orders rename to {table_back_name}") - # drop indexes on backup table - for index in inspector.get_indexes(table_back_name): - engine.execute(f"drop index {index['name']}") + + with engine.begin() as connection: + connection.execute(text(f"alter table orders rename to {table_back_name}")) + # drop indexes on backup table + for index in inspector.get_indexes(table_back_name): + connection.execute(text(f"drop index {index['name']}")) # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) - - engine.execute(f""" - insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, - symbol, order_type, side, price, amount, filled, average, remaining, cost, order_date, - order_filled_date, order_update_date) - select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, - symbol, order_type, side, price, amount, filled, null average, remaining, cost, order_date, - order_filled_date, order_update_date - from {table_back_name} - """) + with engine.begin() as connection: + connection.execute(text(f""" + insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, + status, symbol, order_type, side, price, amount, filled, average, remaining, cost, + order_date, order_filled_date, order_update_date) + select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, + status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, + order_date, order_filled_date, order_update_date + from {table_back_name} + """)) def check_migrate(engine, decl_base, previous_tables) -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6e4030b09..1576aaa5a 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -631,47 +631,49 @@ def test_migrate_new(mocker, default_conf, fee, caplog): caplog.clear() # Drop latest column - engine.execute("alter table orders rename to orders_bak") + with engine.begin() as connection: + connection.execute(text("alter table orders rename to orders_bak")) inspector = inspect(engine) - for index in inspector.get_indexes('orders_bak'): - engine.execute(f"drop index {index['name']}") - # Recreate table - engine.execute(""" - CREATE TABLE orders ( - id INTEGER NOT NULL, - ft_trade_id INTEGER, - ft_order_side VARCHAR NOT NULL, - ft_pair VARCHAR NOT NULL, - ft_is_open BOOLEAN NOT NULL, - order_id VARCHAR NOT NULL, - status VARCHAR, - symbol VARCHAR, - order_type VARCHAR, - side VARCHAR, - price FLOAT, - amount FLOAT, - filled FLOAT, - remaining FLOAT, - cost FLOAT, - order_date DATETIME, - order_filled_date DATETIME, - order_update_date DATETIME, - PRIMARY KEY (id), - CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), - FOREIGN KEY(ft_trade_id) REFERENCES trades (id) - ) - """) + with engine.begin() as connection: + for index in inspector.get_indexes('orders_bak'): + connection.execute(text(f"drop index {index['name']}")) + # Recreate table + connection.execute(text(""" + CREATE TABLE orders ( + id INTEGER NOT NULL, + ft_trade_id INTEGER, + ft_order_side VARCHAR NOT NULL, + ft_pair VARCHAR NOT NULL, + ft_is_open BOOLEAN NOT NULL, + order_id VARCHAR NOT NULL, + status VARCHAR, + symbol VARCHAR, + order_type VARCHAR, + side VARCHAR, + price FLOAT, + amount FLOAT, + filled FLOAT, + remaining FLOAT, + cost FLOAT, + order_date DATETIME, + order_filled_date DATETIME, + order_update_date DATETIME, + PRIMARY KEY (id), + CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), + FOREIGN KEY(ft_trade_id) REFERENCES trades (id) + ) + """)) - engine.execute(""" - insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, - symbol, order_type, side, price, amount, filled, remaining, cost, order_date, - order_filled_date, order_update_date) - select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, - symbol, order_type, side, price, amount, filled, remaining, cost, order_date, - order_filled_date, order_update_date - from orders_bak - """) + connection.execute(text(""" + insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, remaining, cost, order_date, + order_filled_date, order_update_date) + select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, remaining, cost, order_date, + order_filled_date, order_update_date + from orders_bak + """)) # Run init to test migration init_db(default_conf['db_url'], default_conf['dry_run']) From 66de5df1d1dc1962512a20712c90ec88c6c2cb5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 08:56:41 +0200 Subject: [PATCH 0558/1386] Update sqlite init method --- freqtrade/persistence/models.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5648c1aee..ee934f657 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -38,12 +38,14 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ kwargs = {} - # Take care of thread ownership if in-memory db if db_url == 'sqlite://': kwargs.update({ - 'connect_args': {'check_same_thread': False}, 'poolclass': StaticPool, - 'echo': False, + }) + # Take care of thread ownership + if db_url.startswith('sqlite://'): + kwargs.update({ + 'connect_args': {'check_same_thread': False}, }) try: From e0083bc58e384bcd44751c4ab2eacaf90869c92c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 13:00:05 +0200 Subject: [PATCH 0559/1386] Support backwards-compatible sell setting --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 2e0efc8e7..e42b9d4b8 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -261,7 +261,7 @@ CONF_SCHEMA = { 'default': 'off' }, 'sell': { - 'type': 'object', + 'type': ['string', 'object'], 'additionalProperties': { 'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS From cf39dd2163f101d565a849abf545ab4897bae541 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 13:08:28 +0200 Subject: [PATCH 0560/1386] Fix csv-export error with new hyperopt format --- freqtrade/optimize/hyperopt_tools.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 49e70913f..38cb0854e 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -376,10 +376,11 @@ class HyperoptTools(): trials['Avg profit'] = trials['Avg profit'].apply( lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: f'{x:,.1f} m' if isinstance( - x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else "" - ) + if perc_multi == 1: + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: f'{x:,.1f} m' if isinstance( + x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else "" + ) trials['Objective'] = trials['Objective'].apply( lambda x: f'{x:,.5f}' if x != 100000 else "" ) From 639c83575bd0094141f15d4f58814a1a59c565b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 13:08:28 +0200 Subject: [PATCH 0561/1386] Fix csv-export error with new hyperopt format --- freqtrade/optimize/hyperopt_tools.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 49e70913f..38cb0854e 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -376,10 +376,11 @@ class HyperoptTools(): trials['Avg profit'] = trials['Avg profit'].apply( lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: f'{x:,.1f} m' if isinstance( - x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else "" - ) + if perc_multi == 1: + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: f'{x:,.1f} m' if isinstance( + x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else "" + ) trials['Objective'] = trials['Objective'].apply( lambda x: f'{x:,.5f}' if x != 100000 else "" ) From 9fbc5c05378c84759da9d41ea39557c21df5e6fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 14:00:13 +0200 Subject: [PATCH 0562/1386] Switch to pyproject.toml for setup --- pyproject.toml | 27 ++++++++++ setup.cfg | 40 +++++++++++++++ setup.py | 130 ++++++++++++++++++------------------------------- 3 files changed, 114 insertions(+), 83 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..8199e446a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[tool.black] +line-length = 100 +exclude = ''' +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + )/ + # Exclude vendor directory + | vendor +) +''' + +[tool.isort] +line_length = 100 + +[build-system] +requires = ["setuptools >= 46.4.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index be2cd450c..26434791f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,43 @@ +[metadata] +name = freqtrade +version = attr: freqtrade.__version__ +author = Freqtrade Team +author_email = michael.egger@tsn.at +description = Freqtrade - Crypto Trading Bot +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/freqtrade/freqtrade +project_urls = + Bug Tracker = https://github.com/freqtrade/freqtrade/issues +license = GPLv3 +classifiers = + Environment :: Console + Intended Audience :: Science/Research + License :: OSI Approved :: GNU General Public License v3 (GPLv3) + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Operating System :: MacOS + Operating System :: Unix + Topic :: Office/Business :: Financial :: Investment + + +[options] +zip_safe = False +include_package_data = True +tests_require = + pytest + pytest-asyncio + pytest-cov + pytest-mock + +packages = find: +python_requires = >=3.6 + +[options.entry_points] +console_scripts = + freqtrade = freqtrade.main:main + [flake8] #ignore = max-line-length = 100 diff --git a/setup.py b/setup.py index 54a2e01b5..727c40c7c 100644 --- a/setup.py +++ b/setup.py @@ -1,25 +1,7 @@ -from sys import version_info - from setuptools import setup -if version_info.major == 3 and version_info.minor < 7 or \ - version_info.major < 3: - print('Your Python interpreter must be 3.7 or greater!') - exit(1) - -from pathlib import Path # noqa: E402 - -from freqtrade import __version__ # noqa: E402 - - -readme_file = Path(__file__).parent / "README.md" -readme_long = "Crypto Trading Bot" -if readme_file.is_file(): - readme_long = (Path(__file__).parent / "README.md").read_text() - # Requirements used for submodules -api = ['fastapi', 'uvicorn', 'pyjwt', 'aiofiles'] plot = ['plotly>=4.0'] hyperopt = [ 'scipy', @@ -51,69 +33,51 @@ jupyter = [ 'nbconvert', ] -all_extra = api + plot + develop + jupyter + hyperopt +all_extra = plot + develop + jupyter + hyperopt -setup(name='freqtrade', - version=__version__, - description='Crypto Trading Bot', - long_description=readme_long, - long_description_content_type="text/markdown", - url='https://github.com/freqtrade/freqtrade', - author='Freqtrade Team', - author_email='michael.egger@tsn.at', - license='GPLv3', - packages=['freqtrade'], - setup_requires=['pytest-runner', 'numpy'], - tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ], - install_requires=[ - # from requirements.txt - 'ccxt>=1.24.96', - 'SQLAlchemy', - 'python-telegram-bot>=13.4', - 'arrow>=0.17.0', - 'cachetools', - 'requests', - 'urllib3', - 'wrapt', - 'jsonschema', - 'TA-Lib', - 'technical', - 'tabulate', - 'pycoingecko', - 'py_find_1st', - 'python-rapidjson', - 'sdnotify', - 'colorama', - 'jinja2', - 'questionary', - 'prompt-toolkit', - 'numpy', - 'pandas', - 'tables', - 'blosc', - ], - extras_require={ - 'api': api, - 'dev': all_extra, - 'plot': plot, - 'jupyter': jupyter, - 'hyperopt': hyperopt, - 'all': all_extra, - }, - include_package_data=True, - zip_safe=False, - entry_points={ - 'console_scripts': [ - 'freqtrade = freqtrade.main:main', - ], - }, - classifiers=[ - 'Environment :: Console', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Operating System :: MacOS', - 'Operating System :: Unix', - 'Topic :: Office/Business :: Financial :: Investment', - ]) +setup( + tests_require=[ + 'pytest', + 'pytest-asyncio', + 'pytest-cov', + 'pytest-mock', + ], + install_requires=[ + # from requirements.txt + 'ccxt>=1.50.48', + 'SQLAlchemy', + 'python-telegram-bot>=13.4', + 'arrow>=0.17.0', + 'cachetools', + 'requests', + 'urllib3', + 'wrapt', + 'jsonschema', + 'TA-Lib', + 'technical', + 'tabulate', + 'pycoingecko', + 'py_find_1st', + 'python-rapidjson', + 'sdnotify', + 'colorama', + 'jinja2', + 'questionary', + 'prompt-toolkit', + 'numpy', + 'pandas', + 'tables', + 'blosc', + 'fastapi', + 'uvicorn', + 'pyjwt', + 'aiofiles' + ], + extras_require={ + 'dev': all_extra, + 'plot': plot, + 'jupyter': jupyter, + 'hyperopt': hyperopt, + 'all': all_extra, + }, +) From 3014bc34673bd8b5b32c46162d7313c0a51139ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 14:22:11 +0200 Subject: [PATCH 0563/1386] Don't use Sum sign in hyperopt to avoid compatibility problems --- docs/advanced-hyperopt.md | 2 +- docs/hyperopt.md | 8 ++++---- freqtrade/optimize/hyperopt.py | 2 +- tests/optimize/test_hyperopt.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index c86978b80..35fd3de4a 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -289,7 +289,7 @@ Given the following result from hyperopt: ``` Best result: - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722%). Avg duration 180.4 mins. Objective: 1.94367 Buy hyperspace params: { 'adx-value': 44, diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b3fdc699b..1c95d84c9 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -486,7 +486,7 @@ Given the following result from hyperopt: ``` Best result: - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722%). Avg duration 180.4 mins. Objective: 1.94367 # Buy hyperspace params: buy_params = { @@ -527,7 +527,7 @@ If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'de ``` Best result: - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722%). Avg duration 180.4 mins. Objective: 1.94367 # ROI table: minimal_roi = { @@ -582,7 +582,7 @@ If you are optimizing stoploss values (i.e. if optimization search-space contain ``` Best result: - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722%). Avg duration 180.4 mins. Objective: 1.94367 # Buy hyperspace params: buy_params = { @@ -624,7 +624,7 @@ If you are optimizing trailing stop values (i.e. if optimization search-space co ``` Best result: - 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 + 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48%). Avg duration 150.3 mins. Objective: -1.10161 # Trailing stop: trailing_stop = True diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6f899aba6..9af3710a3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -351,7 +351,7 @@ class Hyperopt: f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " f"Median profit {results_metrics['median_profit']: 6.2f}%. " f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " - f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " + f"({results_metrics['profit']: 7.2f}%). " f"Avg duration {results_metrics['duration']:5.1f} min." ) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 637623e7d..7cb04fae0 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -628,7 +628,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'loss': 1.9840569076926293, 'results_explanation': (' 1 trades. 1/0/0 Wins/Draws/Losses. ' 'Avg profit 2.31%. Median profit 2.31%. Total profit ' - '0.00023300 BTC ( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). ' + '0.00023300 BTC ( 2.31%). ' 'Avg duration 100.0 min.' ), 'params_details': {'buy': {'adx-enabled': False, From 5d961074965a5d9979a622cdf6a76a754788ca41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 14:25:29 +0200 Subject: [PATCH 0564/1386] Don't configure isort twice --- pyproject.toml | 2 ++ setup.cfg | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8199e446a..f0637d8c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ exclude = ''' [tool.isort] line_length = 100 +multi_line_output=0 +lines_after_imports=2 [build-system] requires = ["setuptools >= 46.4.0", "wheel"] diff --git a/setup.cfg b/setup.cfg index 26434791f..4882c29bc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,11 +48,6 @@ exclude = .eggs, user_data, -[isort] -line_length=100 -multi_line_output=0 -lines_after_imports=2 - [mypy] ignore_missing_imports = True From 6235a4d92eff3ec4f4e0eaf5723674466be50566 Mon Sep 17 00:00:00 2001 From: Marijn Date: Thu, 27 May 2021 15:01:58 +0200 Subject: [PATCH 0565/1386] [changes] - Hyperopt code example --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0d7b1cc32..5f4e60b4a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -237,9 +237,9 @@ class MyAwesomeStrategy(IStrategy): dataframe['macdhist'] = macd['macdhist'] bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) - dataframe['bb_lowerband'] = boll['lowerband'] - dataframe['bb_middleband'] = boll['middleband'] - dataframe['bb_upperband'] = boll['upperband'] + dataframe['bb_lowerband'] = bollinger['lowerband'] + dataframe['bb_middleband'] = bollinger['middleband'] + dataframe['bb_upperband'] = bollinger['upperband'] return dataframe ``` From a42effd4fcc72b4033a38a934a3d8949096d666f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 May 2021 08:26:20 +0200 Subject: [PATCH 0566/1386] Update email to freqtrade email address --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4882c29bc..b311c94da 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ name = freqtrade version = attr: freqtrade.__version__ author = Freqtrade Team -author_email = michael.egger@tsn.at +author_email = freqtrade@protonmail.com description = Freqtrade - Crypto Trading Bot long_description = file: README.md long_description_content_type = text/markdown From 8a56af919225f881916f0c008f672de9f174d967 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 May 2021 08:38:46 +0200 Subject: [PATCH 0567/1386] Update onlyprofit loss should use absolute profit closes #4934 --- freqtrade/optimize/hyperopt_loss_onlyprofit.py | 18 +++--------------- tests/optimize/test_hyperoptloss.py | 4 ++-- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_onlyprofit.py b/freqtrade/optimize/hyperopt_loss_onlyprofit.py index 33f3f5bc6..4a3cf1b3b 100644 --- a/freqtrade/optimize/hyperopt_loss_onlyprofit.py +++ b/freqtrade/optimize/hyperopt_loss_onlyprofit.py @@ -9,23 +9,11 @@ from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss -# This is assumed to be expected avg profit * expected trade count. -# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# expected max profit = 3.85 -# -# Note, this is ratio. 3.85 stated above means 385Σ%, 3.0 means 300Σ%. -# -# In this implementation it's only used in calculation of the resulting value -# of the objective function as a normalization coefficient and does not -# represent any limit for profits as in the Freqtrade legacy default loss function. -EXPECTED_MAX_PROFIT = 3.0 - - class OnlyProfitHyperOptLoss(IHyperOptLoss): """ Defines the loss function for hyperopt. - This implementation takes only profit into account. + This implementation takes only absolute profit into account, not looking at any other indicator. """ @staticmethod @@ -34,5 +22,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss): """ Objective function, returns smaller number for better results. """ - total_profit = results['profit_ratio'].sum() - return 1 - total_profit / EXPECTED_MAX_PROFIT + total_profit = results['profit_abs'].sum() + return -1 * total_profit diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 73feeb007..ea0caac04 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -149,9 +149,9 @@ def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_result def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() - results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 + results_over['profit_abs'] = hyperopt_results['profit_abs'] * 2 results_under = hyperopt_results.copy() - results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 + results_under['profit_abs'] = hyperopt_results['profit_abs'] / 2 default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) From a965436cd6443f41ed0f906eed8b8a025de7e0ea Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 10:17:26 +0300 Subject: [PATCH 0568/1386] day/week options for Telegram '/profit' command format changed to "/profit n" --- README.md | 2 +- docs/telegram-usage.md | 4 ++-- freqtrade/rpc/telegram.py | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ada31b42b..2730ee85d 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor - `/stop`: Stops the trader. - `/stopbuy`: Stop entering new trades. - `/status |[table]`: Lists all or specific open trades. -- `/profit [day]|[week]`: Lists cumulative profit from all finished trades +- `/profit []`: Lists cumulative profit from all finished trades, over the last n days. - `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/performance`: Show performance of each finished trade grouped by pair - `/balance`: Show account balance per currency. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index c477921de..0e6bae380 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -130,7 +130,7 @@ You can create your own keyboard in `config.json`: !!! Note "Supported Commands" Only the following commands are allowed. Command arguments are not supported! - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/profit day`, `/profit week`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` ## Telegram commands @@ -154,7 +154,7 @@ official commands. You can ask at any moment for help with `/help`. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). -| `/profit [day]|[week]` | Display a summary of your profit/loss from close trades and some stats about your performance +| `/profit []` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `/forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 19c520efa..4be990b96 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -206,13 +206,14 @@ class Telegram(RPCHandler): msg['emoji'] = self._get_sell_emoji(msg) message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Profit:* `{profit_percent:.2f}%`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) + "*Close Rate:* `{limit:.8f}`" + ).format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell @@ -423,11 +424,11 @@ class Telegram(RPCHandler): fiat_disp_cur = self._config.get('fiat_display_currency', '') start_date = datetime.fromtimestamp(0) - if context.args: - if 'day' in context.args: - start_date = datetime.combine(date.today(), datetime.min.time()) - elif 'week' in context.args: - start_date = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=7) + try: + timescale = int(context.args[0]) if context.args else None + start_date = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=timescale) + except (TypeError, ValueError, IndexError): + pass stats = self._rpc._rpc_trade_statistics( stake_cur, From 27bd3cea4f15b19507dca486e3486ef019ffa7e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 May 2021 12:40:30 +0200 Subject: [PATCH 0569/1386] Fix failing docker build --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7d5afac9c..b12cde6bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ USER ftuser # Install and execute COPY --chown=ftuser:ftuser . /freqtrade/ -RUN pip install -e . --user --no-cache-dir \ +RUN pip install -e . --user --no-cache-dir --no-build-isolation \ && mkdir /freqtrade/user_data/ \ && freqtrade install-ui From 59366208b0fdf096706f6dd599fe5e9635445aaf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 May 2021 13:01:09 +0200 Subject: [PATCH 0570/1386] Add no_build-isolation to arm images too --- Dockerfile.armhf | 2 +- docker/Dockerfile.aarch64 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 2b3bca042..9b7986240 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -49,7 +49,7 @@ USER ftuser # Install and execute COPY --chown=ftuser:ftuser . /freqtrade/ -RUN pip install -e . --user --no-cache-dir \ +RUN pip install -e . --user --no-cache-dir --no-build-isolation\ && mkdir /freqtrade/user_data/ \ && freqtrade install-ui diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 index 71c10d949..9f51ba61e 100644 --- a/docker/Dockerfile.aarch64 +++ b/docker/Dockerfile.aarch64 @@ -49,7 +49,7 @@ USER ftuser # Install and execute COPY --chown=ftuser:ftuser . /freqtrade/ -RUN pip install -e . --user --no-cache-dir \ +RUN pip install -e . --user --no-cache-dir --no-build-isolation\ && mkdir /freqtrade/user_data/ \ && freqtrade install-ui From 4b5a9d8c497f2a093cd226dcdf9835de2e9550f2 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 14:43:57 +0300 Subject: [PATCH 0571/1386] day/week options for Telegram '/profit' command revert accidental changes --- freqtrade/rpc/telegram.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4be990b96..ef13d25f0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -206,14 +206,13 @@ class Telegram(RPCHandler): msg['emoji'] = self._get_sell_emoji(msg) message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`" - ).format(**msg) + "*Close Rate:* `{limit:.8f}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell From 36b68d3702e9497cd1a7bd36779d0ad46676f6dd Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 14:46:22 +0300 Subject: [PATCH 0572/1386] day/week options for Telegram '/profit' command format changed to "/profit n" --- 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 ef13d25f0..27dea30fd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -910,7 +910,7 @@ class Telegram(RPCHandler): " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" - "*/profit [day]|[week]:* `Lists cumulative profit from all finished trades`\n" + "*/profit []:* `Lists cumulative profit from all finished trades, over the last n days`\n" "*/forcesell |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" From 012309a06a84d556b1cbece295b92a2585c9585e Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 17:03:31 +0300 Subject: [PATCH 0573/1386] day/week options for Telegram '/profit' command fixed line lenght --- freqtrade/rpc/telegram.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 27dea30fd..b86f1b29c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -425,7 +425,8 @@ class Telegram(RPCHandler): start_date = datetime.fromtimestamp(0) try: timescale = int(context.args[0]) if context.args else None - start_date = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=timescale) + today_start = datetime.combine(date.today(), datetime.min.time()) + start_date = today_start - timedelta(days=timescale) except (TypeError, ValueError, IndexError): pass @@ -910,7 +911,8 @@ class Telegram(RPCHandler): " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" - "*/profit []:* `Lists cumulative profit from all finished trades, over the last n days`\n" + "*/profit []:* `Lists cumulative profit from all finished trades, " + "over the last n days`\n" "*/forcesell |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" From 14df243661f817fb7ade4155cbb91e97e3e89cc6 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 17:18:23 +0300 Subject: [PATCH 0574/1386] day/week options for Telegram '/profit' command mypy fix --- freqtrade/rpc/telegram.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b86f1b29c..db709c556 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -424,9 +424,10 @@ class Telegram(RPCHandler): start_date = datetime.fromtimestamp(0) try: - timescale = int(context.args[0]) if context.args else None - today_start = datetime.combine(date.today(), datetime.min.time()) - start_date = today_start - timedelta(days=timescale) + if context.args: + timescale = int(context.args[0]) + today_start = datetime.combine(date.today(), datetime.min.time()) + start_date = today_start - timedelta(days=timescale) except (TypeError, ValueError, IndexError): pass From 4617967e14fc01e82c5dc14ea3e37fddf8540963 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 May 2021 21:47:40 +0200 Subject: [PATCH 0575/1386] Try building for multiarch --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a0837eb2..ab640789e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - master - stable - develop + - test_multiarch tags: release: types: [published] @@ -402,7 +403,7 @@ jobs: - name: Build Raspberry docker image env: IMAGE_NAME: freqtradeorg/freqtrade - BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}_pi + BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} run: | build_helpers/publish_docker_pi.sh From 6418f2eedb154854039e3613894b304f92131ab0 Mon Sep 17 00:00:00 2001 From: Jyothish Kumar M S Date: Sat, 29 May 2021 01:28:20 +0530 Subject: [PATCH 0576/1386] Removed binance.je from exchange specific notes Binance Jersey is deprecated, so I think it should be removed from freqtrade --- docs/exchanges.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 8797ade8c..4a60c7683 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -14,11 +14,10 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ ### Binance sites -Binance has been split into 3, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. +Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. * [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. * [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. -* [binance.je](https://www.binance.je/) - Binance Jersey, trading fiat currencies. Use exchange id: `binanceje`. ## Kraken From 313567d07d0616bd40bb5b785dbf61eeb8d40aea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 May 2021 08:12:25 +0200 Subject: [PATCH 0577/1386] Support having numbers in custom keyboard --- freqtrade/rpc/telegram.py | 22 +++++++++++++--------- tests/rpc/test_rpc_telegram.py | 4 ++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index db709c556..6c7fa0493 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,6 +5,7 @@ This module manage Telegram communication """ import json import logging +import re from datetime import date, datetime, timedelta from html import escape from itertools import chain @@ -97,24 +98,27 @@ class Telegram(RPCHandler): # TODO: DRY! - its not good to list all valid cmds here. But otherwise # this needs refacoring of the whole telegram module (same # problem in _help()). - valid_keys: List[str] = ['/start', '/stop', '/status', '/status table', - '/trades', '/performance', '/daily', - '/profit', '/profit day', '/profit week', - '/stats', '/count', '/locks', '/balance', - '/stopbuy', '/reload_config', '/show_config', - '/logs', '/whitelist', '/blacklist', '/edge', - '/help', '/version'] + valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$', + r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$', + r'/profit$', r'/profit \d+', + r'/stats$', r'/count$', r'/locks$', r'/balance$', + r'/stopbuy$', r'/reload_config$', r'/show_config$', + r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', + r'/forcebuy$', r'/help$', r'/version$'] + # Create keys for generation + valid_keys_print = [k.replace('$', '') for k in valid_keys] # custom keyboard specified in config.json cust_keyboard = self._config['telegram'].get('keyboard', []) if cust_keyboard: + combined = "(" + ")|(".join(valid_keys) + ")" # check for valid shortcuts invalid_keys = [b for b in chain.from_iterable(cust_keyboard) - if b not in valid_keys] + if not re.match(combined, b)] if len(invalid_keys): err_msg = ('config.telegram.keyboard: Invalid commands for ' f'custom Telegram keyboard: {invalid_keys}' - f'\nvalid commands are: {valid_keys}') + f'\nvalid commands are: {valid_keys_print}') raise OperationalException(err_msg) else: self._keyboard = cust_keyboard diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6008ede66..a9af498b7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1568,7 +1568,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: ['/count', '/start', '/stop', '/help']] default_keyboard = ReplyKeyboardMarkup(default_keys_list) - custom_keys_list = [['/daily', '/stats', '/balance', '/profit'], + custom_keys_list = [['/daily', '/stats', '/balance', '/profit', '/profit 5'], ['/count', '/start', '/reload_config', '/help']] custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) @@ -1602,5 +1602,5 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard assert log_has("using custom keyboard from config.json: " - "[['/daily', '/stats', '/balance', '/profit'], ['/count', " + "[['/daily', '/stats', '/balance', '/profit', '/profit 5'], ['/count', " "'/start', '/reload_config', '/help']]", caplog) From 8658be004e00eb0c75fe364407ed54d2d8dd46bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 06:35:41 +0200 Subject: [PATCH 0578/1386] Use docker-manifest to build multiarch images --- build_helpers/publish_docker_pi.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/build_helpers/publish_docker_pi.sh b/build_helpers/publish_docker_pi.sh index 060b1deaf..d3a941a1f 100755 --- a/build_helpers/publish_docker_pi.sh +++ b/build_helpers/publish_docker_pi.sh @@ -3,7 +3,9 @@ # The below assumes a correctly setup docker buildx environment # Replace / with _ to create a valid tag -TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") +TAG_ORIG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") +TAG="${TAG_ORIG}_pi" + PI_PLATFORM="linux/arm/v7" echo "Running for ${TAG}" CACHE_TAG=freqtradeorg/freqtrade_cache:${TAG}_cache @@ -30,6 +32,13 @@ else -t ${IMAGE_NAME}:${TAG} --push . fi +docker images + +docker manifest create freqtradeorg/freqtrade:${TAG}_multi ${IMAGE_NAME}:${TAG_ORIG} ${IMAGE_NAME}:${TAG} +docker manifest push freqtradeorg/freqtrade:${TAG}_multi + +docker images + if [ $? -ne 0 ]; then echo "failed building image" return 1 From 1e052bde90d8a511f3247b40733b4a5b1e337452 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 10:23:08 +0200 Subject: [PATCH 0579/1386] Move Dockerfile.armhf to docker directory --- .dockerignore | 1 + .github/workflows/ci.yml | 14 +++++++------- build_helpers/publish_docker.sh | 1 + build_helpers/publish_docker_pi.sh | 10 +++++----- Dockerfile.armhf => docker/Dockerfile.armhf | 0 5 files changed, 14 insertions(+), 12 deletions(-) rename Dockerfile.armhf => docker/Dockerfile.armhf (100%) diff --git a/.dockerignore b/.dockerignore index 889a4dfc7..abc5b82f0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ Dockerfile Dockerfile.armhf .dockerignore +docker/ .coveragerc .eggs .github diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab640789e..dbcde8adf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -375,13 +375,6 @@ jobs: run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Build and test and push docker image - env: - IMAGE_NAME: freqtradeorg/freqtrade - BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} - run: | - build_helpers/publish_docker.sh - # We need docker experimental to pull the ARM image. - name: Switch docker to experimental run: | @@ -400,6 +393,13 @@ jobs: - name: Available platforms run: echo ${{ steps.buildx.outputs.platforms }} + - name: Build and test and push docker image + env: + IMAGE_NAME: freqtradeorg/freqtrade + BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} + run: | + build_helpers/publish_docker.sh + - name: Build Raspberry docker image env: IMAGE_NAME: freqtradeorg/freqtrade diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index d987bcc69..da9fc4e34 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -53,6 +53,7 @@ docker images docker push ${IMAGE_NAME} docker push ${IMAGE_NAME}:$TAG_PLOT docker push ${IMAGE_NAME}:$TAG + if [ $? -ne 0 ]; then echo "failed pushing repo" return 1 diff --git a/build_helpers/publish_docker_pi.sh b/build_helpers/publish_docker_pi.sh index d3a941a1f..7c77c5ba1 100755 --- a/build_helpers/publish_docker_pi.sh +++ b/build_helpers/publish_docker_pi.sh @@ -7,7 +7,7 @@ TAG_ORIG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG="${TAG_ORIG}_pi" PI_PLATFORM="linux/arm/v7" -echo "Running for ${TAG}" +echo "Running for ${TAG_ORIG}" CACHE_TAG=freqtradeorg/freqtrade_cache:${TAG}_cache # Add commit and commit_message to docker container @@ -17,7 +17,7 @@ if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache" docker buildx build \ --cache-to=type=registry,ref=${CACHE_TAG} \ - -f Dockerfile.armhf \ + -f docker/Dockerfile.armhf \ --platform ${PI_PLATFORM} \ -t ${IMAGE_NAME}:${TAG} --push . else @@ -27,15 +27,15 @@ else docker buildx build \ --cache-from=type=registry,ref=${CACHE_TAG} \ --cache-to=type=registry,ref=${CACHE_TAG} \ - -f Dockerfile.armhf \ + -f docker/Dockerfile.armhf \ --platform ${PI_PLATFORM} \ -t ${IMAGE_NAME}:${TAG} --push . fi docker images -docker manifest create freqtradeorg/freqtrade:${TAG}_multi ${IMAGE_NAME}:${TAG_ORIG} ${IMAGE_NAME}:${TAG} -docker manifest push freqtradeorg/freqtrade:${TAG}_multi +docker manifest create freqtradeorg/freqtrade:${TAG_ORIG} ${IMAGE_NAME}:${TAG_ORIG} ${IMAGE_NAME}:${TAG} +docker manifest push freqtradeorg/freqtrade:${TAG_ORIG} docker images diff --git a/Dockerfile.armhf b/docker/Dockerfile.armhf similarity index 100% rename from Dockerfile.armhf rename to docker/Dockerfile.armhf From 9cf2c2201b61a2ddbdb47aa622d0c1b4cfc74dd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 May 2021 15:51:16 +0200 Subject: [PATCH 0580/1386] Align dockerfiles --- Dockerfile | 12 ++++++------ build_helpers/publish_docker_pi.sh | 6 +++++- docker/Dockerfile.aarch64 | 6 +++--- docker/Dockerfile.armhf | 5 +++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index b12cde6bb..f2d7c8a40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,8 @@ ENV FT_APP_ENV="docker" # Prepare environment RUN mkdir /freqtrade \ - && apt update \ - && apt install -y sudo \ + && apt-get update \ + && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \ && apt-get clean \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ @@ -22,10 +22,10 @@ WORKDIR /freqtrade # Install dependencies FROM base as python-deps -RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev git \ - && apt-get clean \ - && pip install --upgrade pip +RUN apt-get update \ + && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ + && apt-get clean \ + && pip install --upgrade pip # Install TA-lib COPY build_helpers/* /tmp/ diff --git a/build_helpers/publish_docker_pi.sh b/build_helpers/publish_docker_pi.sh index 7c77c5ba1..c7024828b 100755 --- a/build_helpers/publish_docker_pi.sh +++ b/build_helpers/publish_docker_pi.sh @@ -7,7 +7,7 @@ TAG_ORIG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG="${TAG_ORIG}_pi" PI_PLATFORM="linux/arm/v7" -echo "Running for ${TAG_ORIG}" +echo "Running for ${TAG}" CACHE_TAG=freqtradeorg/freqtrade_cache:${TAG}_cache # Add commit and commit_message to docker container @@ -34,6 +34,10 @@ fi docker images +# Create multiarch image +# Make sure that all images contained here are pushed to github first. +# Otherwise installation might fail. + docker manifest create freqtradeorg/freqtrade:${TAG_ORIG} ${IMAGE_NAME}:${TAG_ORIG} ${IMAGE_NAME}:${TAG} docker manifest push freqtradeorg/freqtrade:${TAG_ORIG} diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 index 9f51ba61e..e5d3f0ee9 100644 --- a/docker/Dockerfile.aarch64 +++ b/docker/Dockerfile.aarch64 @@ -11,7 +11,7 @@ ENV FT_APP_ENV="docker" # Prepare environment RUN mkdir /freqtrade \ && apt-get update \ - && apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \ + && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \ && apt-get clean \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ @@ -22,8 +22,8 @@ WORKDIR /freqtrade # Install dependencies FROM base as python-deps -RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ +RUN apt-get update \ + && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get clean \ && pip install --upgrade pip diff --git a/docker/Dockerfile.armhf b/docker/Dockerfile.armhf index 9b7986240..8abf0e44b 100644 --- a/docker/Dockerfile.armhf +++ b/docker/Dockerfile.armhf @@ -11,7 +11,7 @@ ENV FT_APP_ENV="docker" # Prepare environment RUN mkdir /freqtrade \ && apt-get update \ - && apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \ + && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \ && apt-get clean \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ @@ -22,7 +22,8 @@ WORKDIR /freqtrade # Install dependencies FROM base as python-deps -RUN apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 \ +RUN apt-get update \ + && apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf From f6b1abe23fe40fe6f51659b33894447c8eddf7cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 May 2021 08:22:50 +0200 Subject: [PATCH 0581/1386] Remove ci from test_multiarch again --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbcde8adf..ea766d77d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: - master - stable - develop - - test_multiarch tags: release: types: [published] From d7fdc2114ac6b9c5b4fd9d1eaa0807a59ef169f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 May 2021 13:02:18 +0200 Subject: [PATCH 0582/1386] allow list-strategies to show if params are hyperoptable --- freqtrade/commands/list_commands.py | 17 ++++++++++++++--- freqtrade/strategy/hyper.py | 22 ++++++++++++++++++---- tests/strategy/test_interface.py | 9 +++++++-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index fa4bc1066..167847a0d 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -54,15 +54,21 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None: reset = '' names = [s['name'] for s in objs] - objss_to_print = [{ + objs_to_print = [{ 'name': s['name'] if s['name'] else "--", 'location': s['location'].name, 'status': (red + "LOAD FAILED" + reset if s['class'] is None else "OK" if names.count(s['name']) == 1 else yellow + "DUPLICATE NAME" + reset) } for s in objs] - - print(tabulate(objss_to_print, headers='keys', tablefmt='psql', stralign='right')) + for idx, s in enumerate(objs): + if 'hyperoptable' in s: + objs_to_print[idx].update({ + 'hyperoptable': "Yes" if s['hyperoptable']['count'] > 0 else "No", + 'buy-Params': len(s['hyperoptable'].get('buy', [])), + 'sell-Params': len(s['hyperoptable'].get('sell', [])), + }) + print(tabulate(objs_to_print, headers='keys', tablefmt='psql', stralign='right')) def start_list_strategies(args: Dict[str, Any]) -> None: @@ -75,6 +81,11 @@ def start_list_strategies(args: Dict[str, Any]) -> None: strategy_objs = StrategyResolver.search_all_objects(directory, not args['print_one_column']) # Sort alphabetically strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) + for obj in strategy_objs: + if obj['class']: + obj['hyperoptable'] = obj['class'].detect_all_parameters() + else: + obj['hyperoptable'] = {'count': 0} if args['print_one_column']: print('\n'.join([s['name'] for s in strategy_objs])) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 7dee47d87..a320f2bb8 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -273,11 +273,12 @@ class HyperStrategyMixin(object): for par in params: yield par.name, par - def _detect_parameters(self, category: str) -> Iterator[Tuple[str, BaseParameter]]: + @classmethod + def detect_parameters(cls, category: str) -> Iterator[Tuple[str, BaseParameter]]: """ Detect all parameters for 'category' """ - for attr_name in dir(self): + for attr_name in dir(cls): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. - attr = getattr(self, attr_name) + attr = getattr(cls, attr_name) if issubclass(attr.__class__, BaseParameter): if (attr_name.startswith(category + '_') and attr.category is not None and attr.category != category): @@ -287,6 +288,19 @@ class HyperStrategyMixin(object): (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 = { + 'buy': list(cls.detect_parameters('buy')), + 'sell': list(cls.detect_parameters('sell')), + } + params.update({ + 'count': len(params['buy'] + params['sell']) + }) + + return params + def _load_hyper_params(self, hyperopt: bool = False) -> None: """ Load Hyperoptable parameters @@ -303,7 +317,7 @@ class HyperStrategyMixin(object): 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 self.detect_parameters(space): attr.name = attr_name attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space) if not attr.category: diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index ded396779..df487986f 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -667,8 +667,13 @@ def test_auto_hyperopt_interface(default_conf): # Parameter is disabled - so value from sell_param dict will NOT be used. assert strategy.sell_minusdi.value == 0.5 + all_params = strategy.detect_all_parameters() + assert isinstance(all_params, dict) + assert len(all_params['buy']) == 2 + assert len(all_params['sell']) == 2 + assert all_params['count'] == 4 - strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy') + 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 strategy.detect_parameters('sell')] From 08f96df3ac94bc99398d84a225236b649648e7e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 May 2021 07:24:03 +0200 Subject: [PATCH 0583/1386] Don't write to testdir, but to tempdir --- tests/commands/test_commands.py | 14 ++++--- tests/data/test_converter.py | 49 +++++++++++++------------ tests/optimize/test_optimize_reports.py | 8 ++-- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 4d3937d87..f47fc46c1 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -919,7 +919,8 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, - saved_hyperopt_results_legacy): + saved_hyperopt_results_legacy, tmpdir): + csv_file = Path(tmpdir) / "test.csv" for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', @@ -1139,17 +1140,18 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, "hyperopt-list", "--no-details", "--no-color", - "--export-csv", "test_file.csv", + "--export-csv", + str(csv_file), ] pargs = get_args(args) pargs['config'] = None start_hyperopt_list(pargs) captured = capsys.readouterr() log_has("CSV file created: test_file.csv", caplog) - f = Path("test_file.csv") - assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() - assert f.is_file() - f.unlink() + assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' + in csv_file.read_text()) + assert csv_file.is_file() + csv_file.unlink() def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 68960af1c..31ce7255e 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -1,5 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +from pathlib import Path +from shutil import copyfile import pytest @@ -11,7 +13,7 @@ from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_forma from freqtrade.data.history import (get_timerange, load_data, load_pair_history, validate_backtest_data) from tests.conftest import log_has, log_has_re -from tests.data.test_history import _backup_file, _clean_test_file +from tests.data.test_history import _clean_test_file def test_dataframe_correct_columns(result): @@ -251,17 +253,19 @@ def test_trades_dict_to_list(fetch_trades_result): assert t[6] == fetch_trades_result[i]['cost'] -def test_convert_trades_format(mocker, default_conf, testdatadir): - files = [{'old': testdatadir / "XRP_ETH-trades.json.gz", - 'new': testdatadir / "XRP_ETH-trades.json"}, - {'old': testdatadir / "XRP_OLD-trades.json.gz", - 'new': testdatadir / "XRP_OLD-trades.json"}, +def test_convert_trades_format(default_conf, testdatadir, tmpdir): + tmpdir1 = Path(tmpdir) + files = [{'old': tmpdir1 / "XRP_ETH-trades.json.gz", + 'new': tmpdir1 / "XRP_ETH-trades.json"}, + {'old': tmpdir1 / "XRP_OLD-trades.json.gz", + 'new': tmpdir1 / "XRP_OLD-trades.json"}, ] for file in files: - _backup_file(file['old'], copy_file=True) + copyfile(testdatadir / file['old'].name, file['old']) + # _backup_file(file['old'], copy_file=True) assert not file['new'].exists() - default_conf['datadir'] = testdatadir + default_conf['datadir'] = tmpdir1 convert_trades_format(default_conf, convert_from='jsongz', convert_to='json', erase=False) @@ -284,14 +288,20 @@ def test_convert_trades_format(mocker, default_conf, testdatadir): file['new'].unlink() -def test_convert_ohlcv_format(mocker, default_conf, testdatadir): - file1 = testdatadir / "XRP_ETH-5m.json" - file1_new = testdatadir / "XRP_ETH-5m.json.gz" - file2 = testdatadir / "XRP_ETH-1m.json" - file2_new = testdatadir / "XRP_ETH-1m.json.gz" - _backup_file(file1, copy_file=True) - _backup_file(file2, copy_file=True) - default_conf['datadir'] = testdatadir +def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir): + tmpdir1 = Path(tmpdir) + + file1_orig = testdatadir / "XRP_ETH-5m.json" + file1 = tmpdir1 / "XRP_ETH-5m.json" + file1_new = tmpdir1 / "XRP_ETH-5m.json.gz" + file2_orig = testdatadir / "XRP_ETH-1m.json" + file2 = tmpdir1 / "XRP_ETH-1m.json" + file2_new = tmpdir1 / "XRP_ETH-1m.json.gz" + + copyfile(file1_orig, file1) + copyfile(file2_orig, file2) + + default_conf['datadir'] = tmpdir1 default_conf['pairs'] = ['XRP_ETH'] default_conf['timeframes'] = ['1m', '5m'] @@ -317,10 +327,3 @@ def test_convert_ohlcv_format(mocker, default_conf, testdatadir): assert file2.exists() assert not file1_new.exists() assert not file2_new.exists() - - _clean_test_file(file1) - _clean_test_file(file2) - if file1_new.exists(): - file1_new.unlink() - if file2_new.exists(): - file2_new.unlink() diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index f9dac3397..f5c9a5a24 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -51,7 +51,7 @@ def test_text_table_bt_results(): assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str -def test_generate_backtest_stats(default_conf, testdatadir): +def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): default_conf.update({'strategy': 'DefaultStrategy'}) StrategyResolver.load_strategy(default_conf) @@ -148,8 +148,8 @@ def test_generate_backtest_stats(default_conf, testdatadir): assert strat_stats['pairlist'] == ['UNITTEST/BTC'] # Test storing stats - filename = Path(testdatadir / 'btresult.json') - filename_last = Path(testdatadir / LAST_BT_RESULT_FN) + filename = Path(tmpdir / 'btresult.json') + filename_last = Path(tmpdir / LAST_BT_RESULT_FN) _backup_file(filename_last, copy_file=True) assert not filename.is_file() @@ -159,7 +159,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): last_fn = get_latest_backtest_filename(filename_last.parent) assert re.match(r"btresult-.*\.json", last_fn) - filename1 = (testdatadir / last_fn) + filename1 = Path(tmpdir / last_fn) assert filename1.is_file() content = filename1.read_text() assert 'max_drawdown' in content From b54da430b96a282458c7c836a7b5c35a345f9c51 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 23 May 2021 11:22:59 +0300 Subject: [PATCH 0584/1386] Add ability to plot bars on indicator chart and pass custom arguments to plotly. --- docs/plotting.md | 14 ++++++++++++-- freqtrade/plot/plotting.py | 28 +++++++++++++++++++++------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 5d454c414..05708ce66 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -170,9 +170,15 @@ Additional features when using plot_config include: * Specify additional subplots * Specify indicator pairs to fill area in between -The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult. +The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult. It also allows multiple subplots to display both MACD and RSI at the same time. +Plot type can be configured using `type` key. Possible types are: +* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default). +* `bar` corresponding to `plotly.graph_objects.Bar` class. + +Extra parameters to `plotly.graph_objects.*` constructor can be specified in `plotly` dict. + Sample configuration with inline comments explaining the process: ``` python @@ -198,7 +204,8 @@ Sample configuration with inline comments explaining the process: # Create subplot MACD "MACD": { 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, - 'macdsignal': {'color': 'orange'} + 'macdsignal': {'color': 'orange'}, + 'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}} }, # Additional subplot RSI "RSI": { @@ -213,6 +220,9 @@ Sample configuration with inline comments explaining the process: The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, `macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy. +!!! Warning + `plotly` arguments are only supported with plotly library and will not work with freq-ui. + ## Plot profit ![plot-profit](assets/plot-profit.png) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index bb4283406..194c20714 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -96,20 +96,34 @@ def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> Dict key must correspond to dataframe column. :param data: candlestick DataFrame """ + plot_kinds = { + 'scatter': go.Scatter, + 'bar': go.Bar, + } for indicator, conf in indicators.items(): logger.debug(f"indicator {indicator} with config {conf}") if indicator in data: kwargs = {'x': data['date'], 'y': data[indicator].values, - 'mode': 'lines', 'name': indicator } - if 'color' in conf: - kwargs.update({'line': {'color': conf['color']}}) - scatter = go.Scatter( - **kwargs - ) - fig.add_trace(scatter, row, 1) + + plot_type = conf.get('type', 'scatter') + color = conf.get('color') + if plot_type == 'bar': + kwargs.update({'marker_color': color or 'DarkSlateGrey', + 'marker_line_color': color or 'DarkSlateGrey'}) + else: + if color: + kwargs.update({'line': {'color': color}}) + kwargs['mode'] = 'lines' + if plot_type != 'scatter': + logger.warning(f'Indicator {indicator} has hnknown plot trace kind {plot_type}' + f', assuming "scatter".') + + kwargs.update(conf.get('plotly', {})) + trace = plot_kinds[plot_type](**kwargs) + fig.add_trace(trace, row, 1) else: logger.info( 'Indicator "%s" ignored. Reason: This indicator is not found ' From 806838c3af9981089af992ec36b90a9a377727ce Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Sun, 30 May 2021 21:07:44 +0700 Subject: [PATCH 0585/1386] Fix we use check sell_noti not noti --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cca87ad91..d1e337401 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -240,8 +240,8 @@ class Telegram(RPCHandler): if msg_type == RPCMessageType.SELL: sell_noti = self._config['telegram'] \ .get('notification_settings', {}).get(str(msg_type), {}) - # For backward compatibility sell still be string - if isinstance(noti, str): + # For backward compatibility sell still can be string + if isinstance(sell_noti, str): noti = sell_noti else: noti = sell_noti.get(str(msg['sell_reason']), default_noti) From 901d984ee33f6abdff702082231189bf1cdae445 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 May 2021 15:50:37 +0200 Subject: [PATCH 0586/1386] Tests should write to tmpdir, not testdir --- tests/data/test_converter.py | 1 - tests/data/test_history.py | 131 +++++++++++++++-------------------- 2 files changed, 54 insertions(+), 78 deletions(-) diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 31ce7255e..802fd4b12 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -262,7 +262,6 @@ def test_convert_trades_format(default_conf, testdatadir, tmpdir): ] for file in files: copyfile(testdatadir / file['old'].name, file['old']) - # _backup_file(file['old'], copy_file=True) assert not file['new'].exists() default_conf['datadir'] = tmpdir1 diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 353cfc6f7..d203d0792 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -86,14 +86,12 @@ def test_load_data_7min_timeframe(mocker, caplog, default_conf, testdatadir) -> def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> None: mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history) file = testdatadir / 'UNITTEST_BTC-1m.json' - _backup_file(file, copy_file=True) load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) assert file.is_file() assert not log_has( 'Download history data for pair: "UNITTEST/BTC", interval: 1m ' 'and store in None.', caplog ) - _clean_test_file(file) def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> None: @@ -112,17 +110,17 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, - default_conf, testdatadir) -> None: + default_conf, tmpdir) -> None: """ Test load_pair_history() with 1 min timeframe """ + tmpdir1 = Path(tmpdir) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) - file = testdatadir / 'MEME_BTC-1m.json' + file = tmpdir1 / 'MEME_BTC-1m.json' - _backup_file(file) # do not download a new pair if refresh_pairs isn't set - load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') + load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC') assert not file.is_file() assert log_has( 'No history data for pair: "MEME/BTC", timeframe: 1m. ' @@ -130,15 +128,14 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'], + refresh_data(datadir=tmpdir1, timeframe='1m', pairs=['MEME/BTC'], exchange=exchange) - load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') + load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC') assert file.is_file() assert log_has_re( 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) - _clean_test_file(file) def test_testdata_path(testdatadir) -> None: @@ -231,26 +228,22 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None: assert start_ts is None -def test_download_pair_history(ohlcv_history_list, mocker, default_conf, testdatadir) -> None: +def test_download_pair_history(ohlcv_history_list, mocker, default_conf, tmpdir) -> None: mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) - file1_1 = testdatadir / 'MEME_BTC-1m.json' - file1_5 = testdatadir / 'MEME_BTC-5m.json' - file2_1 = testdatadir / 'CFI_BTC-1m.json' - file2_5 = testdatadir / 'CFI_BTC-5m.json' - - _backup_file(file1_1) - _backup_file(file1_5) - _backup_file(file2_1) - _backup_file(file2_5) + tmpdir1 = Path(tmpdir) + file1_1 = tmpdir1 / 'MEME_BTC-1m.json' + file1_5 = tmpdir1 / 'MEME_BTC-5m.json' + file2_1 = tmpdir1 / 'CFI_BTC-1m.json' + file2_5 = tmpdir1 / 'CFI_BTC-5m.json' assert not file1_1.is_file() assert not file2_1.is_file() - assert _download_pair_history(datadir=testdatadir, exchange=exchange, + assert _download_pair_history(datadir=tmpdir1, exchange=exchange, pair='MEME/BTC', timeframe='1m') - assert _download_pair_history(datadir=testdatadir, exchange=exchange, + assert _download_pair_history(datadir=tmpdir1, exchange=exchange, pair='CFI/BTC', timeframe='1m') assert not exchange._pairs_last_refresh_time @@ -264,20 +257,16 @@ def test_download_pair_history(ohlcv_history_list, mocker, default_conf, testdat assert not file1_5.is_file() assert not file2_5.is_file() - assert _download_pair_history(datadir=testdatadir, exchange=exchange, + assert _download_pair_history(datadir=tmpdir1, exchange=exchange, pair='MEME/BTC', timeframe='5m') - assert _download_pair_history(datadir=testdatadir, exchange=exchange, + assert _download_pair_history(datadir=tmpdir1, exchange=exchange, pair='CFI/BTC', timeframe='5m') assert not exchange._pairs_last_refresh_time assert file1_5.is_file() assert file2_5.is_file() - # clean files freshly downloaded - _clean_test_file(file1_5) - _clean_test_file(file2_5) - def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: tick = [ @@ -294,24 +283,15 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: assert json_dump_mock.call_count == 2 -def test_download_backtesting_data_exception(ohlcv_history, mocker, caplog, - default_conf, testdatadir) -> None: +def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdir) -> None: mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', side_effect=Exception('File Error')) - + tmpdir1 = Path(tmpdir) exchange = get_patched_exchange(mocker, default_conf) - file1_1 = testdatadir / 'MEME_BTC-1m.json' - file1_5 = testdatadir / 'MEME_BTC-5m.json' - _backup_file(file1_1) - _backup_file(file1_5) - - assert not _download_pair_history(datadir=testdatadir, exchange=exchange, + assert not _download_pair_history(datadir=tmpdir1, exchange=exchange, pair='MEME/BTC', timeframe='1m') - # clean files freshly downloaded - _clean_test_file(file1_1) - _clean_test_file(file1_5) assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog) @@ -528,15 +508,15 @@ def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, tes assert log_has("Skipping pair XRP/ETH...", caplog) -def test_download_trades_history(trades_history, mocker, default_conf, testdatadir, caplog) -> None: - +def test_download_trades_history(trades_history, mocker, default_conf, testdatadir, caplog, + tmpdir) -> None: + tmpdir1 = Path(tmpdir) ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history)) mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', ght_mock) exchange = get_patched_exchange(mocker, default_conf) - file1 = testdatadir / 'ETH_BTC-trades.json.gz' - data_handler = get_datahandler(testdatadir, data_format='jsongz') - _backup_file(file1) + file1 = tmpdir1 / 'ETH_BTC-trades.json.gz' + data_handler = get_datahandler(tmpdir1, data_format='jsongz') assert not file1.is_file() @@ -557,8 +537,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time2 - 5 assert ght_mock.call_args_list[0][1]['from_id'] is not None - # clean files freshly downloaded - _clean_test_file(file1) + file1.unlink() mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', MagicMock(side_effect=ValueError)) @@ -567,9 +546,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad pair='ETH/BTC') assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) - file2 = testdatadir / 'XRP_ETH-trades.json.gz' - - _backup_file(file2, True) + file2 = tmpdir1 / 'XRP_ETH-trades.json.gz' + copyfile(testdatadir / file2.name, file2) ght_mock.reset_mock() mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', @@ -589,38 +567,37 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad _clean_test_file(file2) -def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): - +def test_convert_trades_to_ohlcv(testdatadir, tmpdir, caplog): + tmpdir1 = Path(tmpdir) pair = 'XRP/ETH' - file1 = testdatadir / 'XRP_ETH-1m.json' - file5 = testdatadir / 'XRP_ETH-5m.json' - # Compare downloaded dataset with converted dataset - dfbak_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair) - dfbak_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair) + file1 = tmpdir1 / 'XRP_ETH-1m.json' + file5 = tmpdir1 / 'XRP_ETH-5m.json' + filetrades = tmpdir1 / 'XRP_ETH-trades.json.gz' + copyfile(testdatadir / file1.name, file1) + copyfile(testdatadir / file5.name, file5) + copyfile(testdatadir / filetrades.name, filetrades) - _backup_file(file1, copy_file=True) - _backup_file(file5) + # Compare downloaded dataset with converted dataset + dfbak_1m = load_pair_history(datadir=tmpdir1, timeframe="1m", pair=pair) + dfbak_5m = load_pair_history(datadir=tmpdir1, timeframe="5m", pair=pair) tr = TimeRange.parse_timerange('20191011-20191012') convert_trades_to_ohlcv([pair], timeframes=['1m', '5m'], - datadir=testdatadir, timerange=tr, erase=True) + datadir=tmpdir1, timerange=tr, erase=True) assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) # Load new data - df_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair) - df_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair) + df_1m = load_pair_history(datadir=tmpdir1, timeframe="1m", pair=pair) + df_5m = load_pair_history(datadir=tmpdir1, timeframe="5m", pair=pair) assert df_1m.equals(dfbak_1m) assert df_5m.equals(dfbak_5m) - _clean_test_file(file1) - _clean_test_file(file5) - assert not log_has('Could not convert NoDatapair to OHLCV.', caplog) convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'], - datadir=testdatadir, timerange=tr, erase=True) + datadir=tmpdir1, timerange=tr, erase=True) assert log_has('Could not convert NoDatapair to OHLCV.', caplog) @@ -752,15 +729,17 @@ def test_hdf5datahandler_trades_load(testdatadir): assert len([t for t in trades2 if t[0] > timerange.stopts * 1000]) == 0 -def test_hdf5datahandler_trades_store(testdatadir): +def test_hdf5datahandler_trades_store(testdatadir, tmpdir): + tmpdir1 = Path(tmpdir) dh = HDF5DataHandler(testdatadir) trades = dh.trades_load('XRP/ETH') - dh.trades_store('XRP/NEW', trades) - file = testdatadir / 'XRP_NEW-trades.h5' + dh1 = HDF5DataHandler(tmpdir1) + dh1.trades_store('XRP/NEW', trades) + file = tmpdir1 / 'XRP_NEW-trades.h5' assert file.is_file() # Load trades back - trades_new = dh.trades_load('XRP/NEW') + trades_new = dh1.trades_load('XRP/NEW') assert len(trades_new) == len(trades) assert trades[0][0] == trades_new[0][0] @@ -778,8 +757,6 @@ def test_hdf5datahandler_trades_store(testdatadir): assert trades[-1][5] == trades_new[-1][5] assert trades[-1][6] == trades_new[-1][6] - _clean_test_file(file) - def test_hdf5datahandler_trades_purge(mocker, testdatadir): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) @@ -793,16 +770,18 @@ def test_hdf5datahandler_trades_purge(mocker, testdatadir): assert unlinkmock.call_count == 1 -def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): +def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir, tmpdir): + tmpdir1 = Path(tmpdir) dh = HDF5DataHandler(testdatadir) ohlcv = dh.ohlcv_load('UNITTEST/BTC', '5m') assert isinstance(ohlcv, DataFrame) assert len(ohlcv) > 0 - file = testdatadir / 'UNITTEST_NEW-5m.h5' + file = tmpdir1 / 'UNITTEST_NEW-5m.h5' assert not file.is_file() - dh.ohlcv_store('UNITTEST/NEW', '5m', ohlcv) + dh1 = HDF5DataHandler(tmpdir1) + dh1.ohlcv_store('UNITTEST/NEW', '5m', ohlcv) assert file.is_file() assert not ohlcv[ohlcv['date'] < '2018-01-15'].empty @@ -812,14 +791,12 @@ def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): # Call private function to ensure timerange is filtered in hdf5 ohlcv = dh._ohlcv_load('UNITTEST/BTC', '5m', timerange) - ohlcv1 = dh._ohlcv_load('UNITTEST/NEW', '5m', timerange) + ohlcv1 = dh1._ohlcv_load('UNITTEST/NEW', '5m', timerange) assert len(ohlcv) == len(ohlcv1) assert ohlcv.equals(ohlcv1) assert ohlcv[ohlcv['date'] < '2018-01-15'].empty assert ohlcv[ohlcv['date'] > '2018-01-19'].empty - _clean_test_file(file) - # Try loading inexisting file ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', '5m') assert ohlcv.empty From e3d5c9cb10f8ba6f7eef554c7d13e6e0af895161 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 May 2021 16:39:33 +0100 Subject: [PATCH 0587/1386] Fix typo in exception message --- freqtrade/plot/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 194c20714..b62ae6015 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -118,7 +118,7 @@ def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> kwargs.update({'line': {'color': color}}) kwargs['mode'] = 'lines' if plot_type != 'scatter': - logger.warning(f'Indicator {indicator} has hnknown plot trace kind {plot_type}' + logger.warning(f'Indicator {indicator} has unknown plot trace kind {plot_type}' f', assuming "scatter".') kwargs.update(conf.get('plotly', {})) From 06b59551b018fd4b7d066505e71e2510cd3e3b4e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 May 2021 20:07:57 +0200 Subject: [PATCH 0588/1386] Improve test coverage --- tests/test_configuration.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index b2c883108..c15a0fe6a 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -88,6 +88,18 @@ def test_load_config_file_error_range(default_conf, mocker, caplog) -> None: '"stake_amount": .001, "fiat_display_currency": "USD", ' '"timeframe": "5m", "dry_run": true, "cance') + filedata = json.dumps(default_conf, indent=2).replace( + '"stake_amount": 0.001,', '"stake_amount": .001,') + mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata)) + + x = log_config_error_range('somefile', 'Parse error at offset 4: Invalid value.') + assert isinstance(x, str) + assert (x == ' "max_open_trades": 1,\n "stake_currency": "BTC",\n' + ' "stake_amount": .001,') + + x = log_config_error_range('-', '') + assert x == '' + def test__args_to_config(caplog): From 2d7ccaeb3d019d810558c7a56dc97fcb5454786d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 May 2021 20:14:41 +0200 Subject: [PATCH 0589/1386] Add test for load_config --- freqtrade/configuration/load_config.py | 2 +- tests/test_configuration.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 1320a375f..27190d259 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -43,7 +43,7 @@ def load_file(path: Path) -> Dict[str, Any]: with path.open('r') as file: config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) except FileNotFoundError: - raise OperationalException(f'File file "{path}" not found!') + raise OperationalException(f'File "{path}" not found!') return config diff --git a/tests/test_configuration.py b/tests/test_configuration.py index c15a0fe6a..b08d0775c 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -18,7 +18,7 @@ from freqtrade.configuration.deprecated_settings import (check_conflicting_setti process_deprecated_setting, process_removed_setting, process_temporary_deprecated_settings) -from freqtrade.configuration.load_config import load_config_file, log_config_error_range +from freqtrade.configuration.load_config import load_config_file, load_file, log_config_error_range from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre @@ -101,6 +101,12 @@ def test_load_config_file_error_range(default_conf, mocker, caplog) -> None: assert x == '' +def test_load_file_error(tmpdir): + testpath = Path(tmpdir) / 'config.json' + with pytest.raises(OperationalException, match=r"File .* not found!"): + load_file(testpath) + + def test__args_to_config(caplog): arg_list = ['trade', '--strategy-path', 'TestTest'] From cd300c52ee7d5201b4b27b5e440934d0bacc568f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 05:18:19 +0000 Subject: [PATCH 0590/1386] Bump urllib3 from 1.26.4 to 1.26.5 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 512b2a588..217d751c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-telegram-bot==13.5 arrow==1.1.0 cachetools==4.2.2 requests==2.25.1 -urllib3==1.26.4 +urllib3==1.26.5 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.20 From eb166147c3836b0ae4e3e677eb548eef57354326 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 05:18:39 +0000 Subject: [PATCH 0591/1386] Bump mkdocs-material from 7.1.5 to 7.1.6 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.5 to 7.1.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.5...7.1.6) 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 89011272d..3f8020e8c 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.5 +mkdocs-material==7.1.6 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 From b4319b5ad849cc615e62b2d5406a4d55d0b88633 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 05:18:49 +0000 Subject: [PATCH 0592/1386] Bump sqlalchemy from 1.4.15 to 1.4.17 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.15 to 1.4.17. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 512b2a588..1b0b40698 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.50.48 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.15 +SQLAlchemy==1.4.17 python-telegram-bot==13.5 arrow==1.1.0 cachetools==4.2.2 From 5d4e18233620812f4af1fe93e3665b96063b7564 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 05:18:54 +0000 Subject: [PATCH 0593/1386] Bump coveralls from 3.0.1 to 3.1.0 Bumps [coveralls](https://github.com/TheKevJames/coveralls-python) from 3.0.1 to 3.1.0. - [Release notes](https://github.com/TheKevJames/coveralls-python/releases) - [Changelog](https://github.com/TheKevJames/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/TheKevJames/coveralls-python/compare/3.0.1...3.1.0) 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 4fbf21260..7b106ae9b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==3.0.1 +coveralls==3.1.0 flake8==3.9.2 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.3.0 From f9541d301f767905f4a106e47f1dfdffb95c76b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 08:19:14 +0000 Subject: [PATCH 0594/1386] Bump ccxt from 1.50.48 to 1.50.70 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.50.48 to 1.50.70. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.50.48...1.50.70) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 975adbdbf..8f4fed9ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.3 pandas==1.2.4 -ccxt==1.50.48 +ccxt==1.50.70 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From b5e3fe3b8e08a4ed7fedec2ef81453e57365cafc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 31 May 2021 17:38:41 +0200 Subject: [PATCH 0595/1386] Document bittrex volumepairlist incompatibility closes #5051 --- docs/exchanges.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 4a60c7683..e54f97714 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -53,6 +53,9 @@ Due to the heavy rate-limiting applied by Kraken, the following configuration se Bittrex does not support market orders. If you have a message at the bot startup about this, you should change order type values set in your configuration and/or in the strategy from `"market"` to `"limit"`. See some more details on this [here in the FAQ](faq.md#im-getting-the-exchange-bittrex-does-not-support-market-orders-message-and-cannot-run-my-strategy). +Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment. +Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected. + ### Restricted markets Bittrex split its exchange into US and International versions. From f920c26802c8cccdc0ce4a29cbcae91711a26c55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 31 May 2021 20:00:47 +0200 Subject: [PATCH 0596/1386] fix Hyperopt-list avg-time filters These should use a numeric field (which currently isn't available). closes #5061 --- freqtrade/commands/hyperopt_commands.py | 8 ++++++-- freqtrade/optimize/optimize_reports.py | 19 +++++++++++++------ tests/commands/test_commands.py | 9 +++++---- tests/conftest.py | 8 +++++++- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index e072e12cb..d8b00f369 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -197,8 +197,12 @@ def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: return x['results_metrics']['duration'] else: # New mode - avg = x['results_metrics']['holding_avg'] - return avg.total_seconds() // 60 + if 'holding_avg_s' in x['results_metrics']: + avg = x['results_metrics']['holding_avg_s'] + return avg // 60 + raise OperationalException( + "Holding-average not available. Please omit the filter on average time, " + "or rerun hyperopt with this version") if filteroptions['filter_min_avg_time'] is not None: epochs = _hyperopt_filter_epochs_trade(epochs, 0) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 5822fc627..84e052ac4 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -232,16 +232,23 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) & (results['sell_reason'] == 'trailing_stop_loss')]) + holding_avg = (timedelta(minutes=round(results['trade_duration'].mean())) + if not results.empty else timedelta()) + winner_holding_avg = (timedelta(minutes=round(winning_trades['trade_duration'].mean())) + if not winning_trades.empty else timedelta()) + loser_holding_avg = (timedelta(minutes=round(losing_trades['trade_duration'].mean())) + if not losing_trades.empty else timedelta()) + return { 'wins': len(winning_trades), 'losses': len(losing_trades), 'draws': len(draw_trades), - 'holding_avg': (timedelta(minutes=round(results['trade_duration'].mean())) - if not results.empty else timedelta()), - 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) - if not winning_trades.empty else timedelta()), - 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) - if not losing_trades.empty else timedelta()), + 'holding_avg': holding_avg, + 'holding_avg_s': holding_avg.total_seconds(), + 'winner_holding_avg': winner_holding_avg, + 'winner_holding_avg_s': winner_holding_avg.total_seconds(), + 'loser_holding_avg': loser_holding_avg, + 'loser_holding_avg_s': loser_holding_avg.total_seconds(), 'zero_duration_trades': zero_duration_trades, } diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index f47fc46c1..71ae0ed78 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -921,10 +921,10 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, saved_hyperopt_results_legacy, tmpdir): csv_file = Path(tmpdir) / "test.csv" - for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy): + for res in (saved_hyperopt_results, saved_hyperopt_results_legacy): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=saved_hyperopt_results_legacy) + MagicMock(return_value=res) ) args = [ @@ -1148,9 +1148,10 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, start_hyperopt_list(pargs) captured = capsys.readouterr() log_has("CSV file created: test_file.csv", caplog) - assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' - in csv_file.read_text()) assert csv_file.is_file() + line = csv_file.read_text() + assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line + or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line) csv_file.unlink() diff --git a/tests/conftest.py b/tests/conftest.py index ef2bd0613..43a98647f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1913,7 +1913,7 @@ def saved_hyperopt_results_legacy(): @pytest.fixture def saved_hyperopt_results(): - return [ + hyperopt_res = [ { 'loss': 0.4366182531161519, 'params_dict': { @@ -2042,3 +2042,9 @@ def saved_hyperopt_results(): 'is_best': False } ] + + for res in hyperopt_res: + res['results_metrics']['holding_avg_s'] = res['results_metrics']['holding_avg' + ].total_seconds() + + return hyperopt_res From 53b1f38952a698c2fd5e3661c2125ae0014c1d91 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Tue, 1 Jun 2021 20:08:22 +0300 Subject: [PATCH 0597/1386] telegram: move the most important information to the top of sell message --- freqtrade/rpc/telegram.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d1e337401..ccf19add1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -211,23 +211,26 @@ class Telegram(RPCHandler): msg['emoji'] = self._get_sell_emoji(msg) - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) - # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) and self._rpc._fiat_converter): msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - message += (' `({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) + msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}' + ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) + else: + msg['profit_extra'] = '' + + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`").format(**msg) + return message def send_msg(self, msg: Dict[str, Any]) -> None: From 79552a93fe4924b59cac6d68f2a207f42b2f28d0 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Tue, 1 Jun 2021 20:17:11 +0300 Subject: [PATCH 0598/1386] telegram: move the most important information to the top of sell message fixed tests --- tests/rpc/test_rpc_telegram.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index e640f2dff..4c60bdad3 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1350,13 +1350,14 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' + '*Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' + '*Sell Reason:* `stop_loss`\n' + '*Duration:* `1:00:00 (60.0 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`\n' - '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1:00:00 (60.0 min)`\n' - '*Profit:* `-57.41%` `(loss: -0.05746268 ETH / -24.812 USD)`') + '*Close Rate:* `0.00003201`' + ) msg_mock.reset_mock() telegram.send_msg({ @@ -1379,13 +1380,14 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' + '*Profit:* `-57.41%`\n' + '*Sell Reason:* `stop_loss`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`\n' - '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' - '*Profit:* `-57.41%`') + '*Close Rate:* `0.00003201`' + ) # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount @@ -1537,13 +1539,14 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' + '*Profit:* `-57.41%`\n' + '*Sell Reason:* `stop_loss`\n' + '*Duration:* `2:35:03 (155.1 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`\n' - '*Sell Reason:* `stop_loss`\n' - '*Duration:* `2:35:03 (155.1 min)`\n' - '*Profit:* `-57.41%`') + '*Close Rate:* `0.00003201`' + ) @pytest.mark.parametrize('msg,expected', [ From 1594402312c871b3a3df64eab142d699922e0147 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Jun 2021 19:39:41 +0200 Subject: [PATCH 0599/1386] Add note about signal expiry --- docs/configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index eff8fe322..7fa751bef 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -304,6 +304,9 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy }, ``` +!!! Note + This setting resets with each new candle, so it will not prevent sticking-signals from executing on the 2nd or 3rd candle they're active. Best use a "trigger" selector for buy signals, which are only active for one candle. + ### Understand order_types The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`, `forcebuy`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. From 9edcb393b6704bebbffd0552e029f8126b2c7269 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Tue, 1 Jun 2021 22:24:21 +0300 Subject: [PATCH 0600/1386] telegram: move the most important information to the top of sell message fixed flake error --- 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 ccf19add1..320a3a591 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -218,7 +218,7 @@ class Telegram(RPCHandler): msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) + ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) else: msg['profit_extra'] = '' From 10cd89a99d4e75e7af03fda8b6fbb77cfe573c2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Jun 2021 10:39:49 +0200 Subject: [PATCH 0601/1386] Allow the API to respond faster in case of long pairlists --- freqtrade/freqtradebot.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2e3240cfe..d7369ad47 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -48,6 +48,7 @@ class FreqtradeBot(LoggingMixin): :param config: configuration dict, you can use Configuration.get_config() to get the config dict. """ + self.active_pair_whitelist: List[str] = [] logger.info('Starting freqtrade %s', __version__) @@ -76,12 +77,19 @@ class FreqtradeBot(LoggingMixin): PairLocks.timeframe = self.config['timeframe'] + self.protections = ProtectionManager(self.config) + + # 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 + # the initial state of the bot. + # Keep this at the end of this initialization method. + self.rpc: RPCManager = RPCManager(self) + self.pairlists = PairListManager(self.exchange, self.config) self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) - self.protections = ProtectionManager(self.config) - # Attach Dataprovider to Strategy baseclass IStrategy.dp = self.dataprovider # Attach Wallets to Strategy baseclass @@ -97,12 +105,6 @@ class FreqtradeBot(LoggingMixin): initial_state = self.config.get('initial_state') self.state = State[initial_state.upper()] if initial_state else State.STOPPED - # 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 - # the initial state of the bot. - # Keep this at the end of this initialization method. - self.rpc: RPCManager = RPCManager(self) # Protect sell-logic from forcesell and viceversa self._sell_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) From 67beda6c929df56a1c9861202fdb839e5fb311e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Jun 2021 11:06:32 +0200 Subject: [PATCH 0602/1386] Add fetch_dry_run_order method --- freqtrade/exchange/exchange.py | 28 ++++++++++++++++++---------- freqtrade/exchange/ftx.py | 9 ++------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 93d8f7584..02445de92 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -591,6 +591,19 @@ class Exchange: closed_order["info"].update({"stopPrice": closed_order["price"]}) self._dry_run_open_orders[closed_order["id"]] = closed_order + def fetch_dry_run_order(self, order_id) -> Dict[str, Any]: + """ + Return dry-run order + Only call if running in dry-run mode. + """ + try: + order = self._dry_run_open_orders[order_id] + return order + except KeyError as e: + # Gracefully handle errors with dry-run orders. + raise InvalidOrderException( + f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e + def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict: try: @@ -1066,11 +1079,12 @@ class Exchange: @retrier def cancel_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - order = self._dry_run_open_orders.get(order_id) - if order: + try: + order = self.fetch_dry_run_order(order_id) + order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']}) return order - else: + except InvalidOrderException: return {} try: @@ -1144,13 +1158,7 @@ class Exchange: @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - try: - order = self._dry_run_open_orders[order_id] - return order - except KeyError as e: - # Gracefully handle errors with dry-run orders. - raise InvalidOrderException( - f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e + return self.fetch_dry_run_order(order_id) try: return self._api.fetch_order(order_id, pair) except ccxt.OrderNotFound as e: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 9009e9492..105389828 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -93,13 +93,8 @@ class Ftx(Exchange): @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - try: - order = self._dry_run_open_orders[order_id] - return order - except KeyError as e: - # Gracefully handle errors with dry-run orders. - raise InvalidOrderException( - f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e + return self.fetch_dry_run_order(order_id) + try: orders = self._api.fetch_orders(pair, None, params={'type': 'stop'}) From 4c277b3039bda30369ada1157368dd93d19e4dd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Jun 2021 11:17:50 +0200 Subject: [PATCH 0603/1386] Reorder exchange methods --- freqtrade/exchange/exchange.py | 332 +++++++++++++++++---------------- 1 file changed, 169 insertions(+), 163 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 02445de92..c3c4e8e5a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -550,6 +550,8 @@ class Exchange: # See also #2575 at github. return max(min_stake_amounts) * amount_reserve_percent + # Dry-run methods + def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' @@ -604,6 +606,8 @@ class Exchange: raise InvalidOrderException( f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e + # Order handling + def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict: try: @@ -680,6 +684,128 @@ class Exchange: raise OperationalException(f"stoploss is not implemented for {self.name}.") + @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) + def fetch_order(self, order_id: str, pair: str) -> Dict: + if self._config['dry_run']: + return self.fetch_dry_run_order(order_id) + try: + return self._api.fetch_order(order_id, pair) + except ccxt.OrderNotFound as e: + raise RetryableOrderError( + f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + # Assign method to fetch_stoploss_order to allow easy overriding in other classes + fetch_stoploss_order = fetch_order + + def fetch_order_or_stoploss_order(self, order_id: str, pair: str, + stoploss_order: bool = False) -> Dict: + """ + Simple wrapper calling either fetch_order or fetch_stoploss_order depending on + the stoploss_order parameter + :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order. + """ + if stoploss_order: + return self.fetch_stoploss_order(order_id, pair) + return self.fetch_order(order_id, pair) + + def check_order_canceled_empty(self, order: Dict) -> bool: + """ + Verify if an order has been cancelled without being partially filled + :param order: Order dict as returned from fetch_order() + :return: True if order has been cancelled without being filled, False otherwise. + """ + return (order.get('status') in ('closed', 'canceled', 'cancelled') + and order.get('filled') == 0.0) + + @retrier + def cancel_order(self, order_id: str, pair: str) -> Dict: + if self._config['dry_run']: + try: + order = self.fetch_dry_run_order(order_id) + + order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']}) + return order + except InvalidOrderException: + return {} + + try: + return self._api.cancel_order(order_id, pair) + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + # Assign method to cancel_stoploss_order to allow easy overriding in other classes + cancel_stoploss_order = cancel_order + + def is_cancel_order_result_suitable(self, corder) -> bool: + if not isinstance(corder, dict): + return False + + required = ('fee', 'status', 'amount') + return all(k in corder for k in required) + + def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict: + """ + Cancel order returning a result. + Creates a fake result if cancel order returns a non-usable result + and fetch_order does not work (certain exchanges don't return cancelled orders) + :param order_id: Orderid to cancel + :param pair: Pair corresponding to order_id + :param amount: Amount to use for fake response + :return: Result from either cancel_order if usable, or fetch_order + """ + try: + corder = self.cancel_order(order_id, pair) + if self.is_cancel_order_result_suitable(corder): + return corder + except InvalidOrderException: + logger.warning(f"Could not cancel order {order_id} for {pair}.") + try: + order = self.fetch_order(order_id, pair) + except InvalidOrderException: + logger.warning(f"Could not fetch cancelled order {order_id}.") + order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} + + return order + + def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict: + """ + Cancel stoploss order returning a result. + Creates a fake result if cancel order returns a non-usable result + and fetch_order does not work (certain exchanges don't return cancelled orders) + :param order_id: stoploss-order-id to cancel + :param pair: Pair corresponding to order_id + :param amount: Amount to use for fake response + :return: Result from either cancel_order if usable, or fetch_order + """ + corder = self.cancel_stoploss_order(order_id, pair) + if self.is_cancel_order_result_suitable(corder): + return corder + try: + order = self.fetch_stoploss_order(order_id, pair) + except InvalidOrderException: + logger.warning(f"Could not fetch cancelled stoploss order {order_id}.") + order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} + + return order + @retrier def get_balances(self) -> dict: @@ -726,6 +852,8 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + # Pricing info + @retrier def fetch_ticker(self, pair: str) -> dict: try: @@ -742,6 +870,47 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @staticmethod + def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]], + range_required: bool = True): + """ + Get next greater value in the list. + Used by fetch_l2_order_book if the api only supports a limited range + """ + if not limit_range: + return limit + + result = min([x for x in limit_range if limit <= x] + [max(limit_range)]) + if not range_required and limit > result: + # Range is not required - we can use None as parameter. + return None + return result + + @retrier + def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: + """ + Get L2 order book from exchange. + Can be limited to a certain amount (if supported). + Returns a dict in the format + {'asks': [price, volume], 'bids': [price, volume]} + """ + limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'], + self._ft_has['l2_limit_range_required']) + try: + + return self._api.fetch_l2_order_book(pair, limit1) + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._api.name} does not support fetching order book.' + f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + def get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: """ @@ -1067,169 +1236,6 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - def check_order_canceled_empty(self, order: Dict) -> bool: - """ - Verify if an order has been cancelled without being partially filled - :param order: Order dict as returned from fetch_order() - :return: True if order has been cancelled without being filled, False otherwise. - """ - return (order.get('status') in ('closed', 'canceled', 'cancelled') - and order.get('filled') == 0.0) - - @retrier - def cancel_order(self, order_id: str, pair: str) -> Dict: - if self._config['dry_run']: - try: - order = self.fetch_dry_run_order(order_id) - - order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']}) - return order - except InvalidOrderException: - return {} - - try: - return self._api.cancel_order(order_id, pair) - except ccxt.InvalidOrder as e: - raise InvalidOrderException( - f'Could not cancel order. Message: {e}') from e - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e - - # Assign method to cancel_stoploss_order to allow easy overriding in other classes - cancel_stoploss_order = cancel_order - - def is_cancel_order_result_suitable(self, corder) -> bool: - if not isinstance(corder, dict): - return False - - required = ('fee', 'status', 'amount') - return all(k in corder for k in required) - - def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict: - """ - Cancel order returning a result. - Creates a fake result if cancel order returns a non-usable result - and fetch_order does not work (certain exchanges don't return cancelled orders) - :param order_id: Orderid to cancel - :param pair: Pair corresponding to order_id - :param amount: Amount to use for fake response - :return: Result from either cancel_order if usable, or fetch_order - """ - try: - corder = self.cancel_order(order_id, pair) - if self.is_cancel_order_result_suitable(corder): - return corder - except InvalidOrderException: - logger.warning(f"Could not cancel order {order_id} for {pair}.") - try: - order = self.fetch_order(order_id, pair) - except InvalidOrderException: - logger.warning(f"Could not fetch cancelled order {order_id}.") - order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} - - return order - - def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict: - """ - Cancel stoploss order returning a result. - Creates a fake result if cancel order returns a non-usable result - and fetch_order does not work (certain exchanges don't return cancelled orders) - :param order_id: stoploss-order-id to cancel - :param pair: Pair corresponding to order_id - :param amount: Amount to use for fake response - :return: Result from either cancel_order if usable, or fetch_order - """ - corder = self.cancel_stoploss_order(order_id, pair) - if self.is_cancel_order_result_suitable(corder): - return corder - try: - order = self.fetch_stoploss_order(order_id, pair) - except InvalidOrderException: - logger.warning(f"Could not fetch cancelled stoploss order {order_id}.") - order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} - - return order - - @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) - def fetch_order(self, order_id: str, pair: str) -> Dict: - if self._config['dry_run']: - return self.fetch_dry_run_order(order_id) - try: - return self._api.fetch_order(order_id, pair) - except ccxt.OrderNotFound as e: - raise RetryableOrderError( - f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e - except ccxt.InvalidOrder as e: - raise InvalidOrderException( - f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e - - # Assign method to fetch_stoploss_order to allow easy overriding in other classes - fetch_stoploss_order = fetch_order - - def fetch_order_or_stoploss_order(self, order_id: str, pair: str, - stoploss_order: bool = False) -> Dict: - """ - Simple wrapper calling either fetch_order or fetch_stoploss_order depending on - the stoploss_order parameter - :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order. - """ - if stoploss_order: - return self.fetch_stoploss_order(order_id, pair) - return self.fetch_order(order_id, pair) - - @staticmethod - def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]], - range_required: bool = True): - """ - Get next greater value in the list. - Used by fetch_l2_order_book if the api only supports a limited range - """ - if not limit_range: - return limit - - result = min([x for x in limit_range if limit <= x] + [max(limit_range)]) - if not range_required and limit > result: - # Range is not required - we can use None as parameter. - return None - return result - - @retrier - def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: - """ - Get L2 order book from exchange. - Can be limited to a certain amount (if supported). - Returns a dict in the format - {'asks': [price, volume], 'bids': [price, volume]} - """ - limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'], - self._ft_has['l2_limit_range_required']) - try: - - return self._api.fetch_l2_order_book(pair, limit1) - except ccxt.NotSupported as e: - raise OperationalException( - f'Exchange {self._api.name} does not support fetching order book.' - f'Message: {e}') from e - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e - @retrier def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: """ From 4e1425023e0aa4c0530b5c2ffb8a2e2c20dfd772 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Jun 2021 11:20:26 +0200 Subject: [PATCH 0604/1386] Further reorder exchange methods --- freqtrade/exchange/exchange.py | 242 +++++++++++++++++---------------- 1 file changed, 124 insertions(+), 118 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c3c4e8e5a..7514572fc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -911,6 +911,128 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + # Fee handling + + @retrier + def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: + """ + Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id. + The "since" argument passed in is coming from the database and is in UTC, + as timezone-native datetime object. + From the python documentation: + > Naive datetime instances are assumed to represent local time + Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the + transformation from local timezone to UTC. + This works for timezones UTC+ since then the result will contain trades from a few hours + instead of from the last 5 seconds, however fails for UTC- timezones, + since we're then asking for trades with a "since" argument in the future. + + :param order_id order_id: Order-id as given when creating the order + :param pair: Pair the order is for + :param since: datetime object of the order creation time. Assumes object is in UTC. + """ + if self._config['dry_run']: + return [] + if not self.exchange_has('fetchMyTrades'): + return [] + try: + # Allow 5s offset to catch slight time offsets (discovered in #1185) + # since needs to be int in milliseconds + my_trades = self._api.fetch_my_trades( + pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000)) + matched_trades = [trade for trade in my_trades if trade['order'] == order_id] + + return matched_trades + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + def get_order_id_conditional(self, order: Dict[str, Any]) -> str: + return order['id'] + + @retrier + def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, + price: float = 1, taker_or_maker: str = 'maker') -> float: + try: + if self._config['dry_run'] and self._config.get('fee', None) is not None: + return self._config['fee'] + # validate that markets are loaded before trying to get fee + if self._api.markets is None or len(self._api.markets) == 0: + self._api.load_markets() + + return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, + price=price, takerOrMaker=taker_or_maker)['rate'] + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + @staticmethod + def order_has_fee(order: Dict) -> bool: + """ + Verifies if the passed in order dict has the needed keys to extract fees, + and that these keys (currency, cost) are not empty. + :param order: Order or trade (one trade) dict + :return: True if the fee substructure contains currency and cost, false otherwise + """ + if not isinstance(order, dict): + return False + return ('fee' in order and order['fee'] is not None + and (order['fee'].keys() >= {'currency', 'cost'}) + and order['fee']['currency'] is not None + and order['fee']['cost'] is not None + ) + + def calculate_fee_rate(self, order: Dict) -> Optional[float]: + """ + Calculate fee rate if it's not given by the exchange. + :param order: Order or trade (one trade) dict + """ + if order['fee'].get('rate') is not None: + return order['fee'].get('rate') + fee_curr = order['fee']['currency'] + # Calculate fee based on order details + if fee_curr in self.get_pair_base_currency(order['symbol']): + # Base currency - divide by amount + return round( + order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8) + elif fee_curr in self.get_pair_quote_currency(order['symbol']): + # Quote currency - divide by cost + return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None + else: + # If Fee currency is a different currency + if not order['cost']: + # If cost is None or 0.0 -> falsy, return None + return None + try: + comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency']) + tick = self.fetch_ticker(comb) + + fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask') + return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) + except ExchangeError: + return None + + def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: + """ + Extract tuple of cost, currency, rate. + Requires order_has_fee to run first! + :param order: Order or trade (one trade) dict + :return: Tuple with cost, currency, rate of the given fee dict + """ + return (order['fee']['cost'], + order['fee']['currency'], + self.calculate_fee_rate(order)) + + # Historic data + def get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: """ @@ -1078,6 +1200,8 @@ class Exchange: raise OperationalException(f'Could not fetch historical candle (OHLCV) data ' f'for pair {pair}. Message: {e}') from e + # Fetch historic trades + @retrier_async async def _async_fetch_trades(self, pair: str, since: Optional[int] = None, @@ -1236,124 +1360,6 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @retrier - def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: - """ - Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id. - The "since" argument passed in is coming from the database and is in UTC, - as timezone-native datetime object. - From the python documentation: - > Naive datetime instances are assumed to represent local time - Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the - transformation from local timezone to UTC. - This works for timezones UTC+ since then the result will contain trades from a few hours - instead of from the last 5 seconds, however fails for UTC- timezones, - since we're then asking for trades with a "since" argument in the future. - - :param order_id order_id: Order-id as given when creating the order - :param pair: Pair the order is for - :param since: datetime object of the order creation time. Assumes object is in UTC. - """ - if self._config['dry_run']: - return [] - if not self.exchange_has('fetchMyTrades'): - return [] - try: - # Allow 5s offset to catch slight time offsets (discovered in #1185) - # since needs to be int in milliseconds - my_trades = self._api.fetch_my_trades( - pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000)) - matched_trades = [trade for trade in my_trades if trade['order'] == order_id] - - return matched_trades - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e - - def get_order_id_conditional(self, order: Dict[str, Any]) -> str: - return order['id'] - - @retrier - def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, - price: float = 1, taker_or_maker: str = 'maker') -> float: - try: - if self._config['dry_run'] and self._config.get('fee', None) is not None: - return self._config['fee'] - # validate that markets are loaded before trying to get fee - if self._api.markets is None or len(self._api.markets) == 0: - self._api.load_markets() - - return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, - price=price, takerOrMaker=taker_or_maker)['rate'] - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e - - @staticmethod - def order_has_fee(order: Dict) -> bool: - """ - Verifies if the passed in order dict has the needed keys to extract fees, - and that these keys (currency, cost) are not empty. - :param order: Order or trade (one trade) dict - :return: True if the fee substructure contains currency and cost, false otherwise - """ - if not isinstance(order, dict): - return False - return ('fee' in order and order['fee'] is not None - and (order['fee'].keys() >= {'currency', 'cost'}) - and order['fee']['currency'] is not None - and order['fee']['cost'] is not None - ) - - def calculate_fee_rate(self, order: Dict) -> Optional[float]: - """ - Calculate fee rate if it's not given by the exchange. - :param order: Order or trade (one trade) dict - """ - if order['fee'].get('rate') is not None: - return order['fee'].get('rate') - fee_curr = order['fee']['currency'] - # Calculate fee based on order details - if fee_curr in self.get_pair_base_currency(order['symbol']): - # Base currency - divide by amount - return round( - order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8) - elif fee_curr in self.get_pair_quote_currency(order['symbol']): - # Quote currency - divide by cost - return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None - else: - # If Fee currency is a different currency - if not order['cost']: - # If cost is None or 0.0 -> falsy, return None - return None - try: - comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency']) - tick = self.fetch_ticker(comb) - - fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask') - return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) - except ExchangeError: - return None - - def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: - """ - Extract tuple of cost, currency, rate. - Requires order_has_fee to run first! - :param order: Order or trade (one trade) dict - :return: Tuple with cost, currency, rate of the given fee dict - """ - return (order['fee']['cost'], - order['fee']['currency'], - self.calculate_fee_rate(order)) - def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) From 12916243ecf241bb3332fd36bff844cb49011717 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Jun 2021 11:30:19 +0200 Subject: [PATCH 0605/1386] Move get_buy_rate to exchange class --- freqtrade/exchange/exchange.py | 51 +++++++++++++++++++++++++++++++-- freqtrade/freqtradebot.py | 49 ++----------------------------- tests/exchange/test_exchange.py | 44 ++++++++++++++++++++++++++++ tests/test_freqtradebot.py | 43 --------------------------- 4 files changed, 95 insertions(+), 92 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7514572fc..42b518566 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,8 +22,8 @@ from pandas import DataFrame from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, - InvalidOrderException, OperationalException, RetryableOrderError, - TemporaryError) + InvalidOrderException, OperationalException, PricingError, + RetryableOrderError, TemporaryError) from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier, retrier_async) @@ -88,6 +88,7 @@ class Exchange: # Cache for 10 minutes ... self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) + self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} @@ -911,6 +912,52 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def get_buy_rate(self, pair: str, refresh: bool) -> float: + """ + Calculates bid target between current ask price and last price + :param pair: Pair to get rate for + :param refresh: allow cached data + :return: float: Price + :raises PricingError if orderbook price could not be determined. + """ + if not refresh: + rate = self._buy_rate_cache.get(pair) + # Check if cache has been invalidated + if rate: + logger.debug(f"Using cached buy rate for {pair}.") + return rate + + bid_strategy = self._config.get('bid_strategy', {}) + if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False): + + order_book_top = bid_strategy.get('order_book_top', 1) + order_book = self.fetch_l2_order_book(pair, order_book_top) + logger.debug('order_book %s', order_book) + # top 1 = index 0 + try: + rate_from_l2 = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0] + except (IndexError, KeyError) as e: + logger.warning( + "Buy Price from orderbook could not be determined." + f"Orderbook: {order_book}" + ) + raise PricingError from e + logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side " + f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}") + used_rate = rate_from_l2 + else: + logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price") + ticker = self.fetch_ticker(pair) + ticker_rate = ticker[bid_strategy['price_side']] + if ticker['last'] and ticker_rate > ticker['last']: + balance = bid_strategy['ask_last_balance'] + ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) + used_rate = ticker_rate + + self._buy_rate_cache[pair] = used_rate + + return used_rate + # Fee handling @retrier diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d7369ad47..d15748864 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -62,7 +62,6 @@ class FreqtradeBot(LoggingMixin): # Caching only applies to RPC methods, so prices for open trades are still # refreshed once every iteration. self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) - self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) @@ -396,50 +395,6 @@ class FreqtradeBot(LoggingMixin): return trades_created - def get_buy_rate(self, pair: str, refresh: bool) -> float: - """ - Calculates bid target between current ask price and last price - :param pair: Pair to get rate for - :param refresh: allow cached data - :return: float: Price - """ - if not refresh: - rate = self._buy_rate_cache.get(pair) - # Check if cache has been invalidated - if rate: - logger.debug(f"Using cached buy rate for {pair}.") - return rate - - bid_strategy = self.config.get('bid_strategy', {}) - if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False): - - order_book_top = bid_strategy.get('order_book_top', 1) - order_book = self.exchange.fetch_l2_order_book(pair, order_book_top) - logger.debug('order_book %s', order_book) - # top 1 = index 0 - try: - rate_from_l2 = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0] - except (IndexError, KeyError) as e: - logger.warning( - "Buy Price from orderbook could not be determined." - f"Orderbook: {order_book}" - ) - raise PricingError from e - logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side " - f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}") - used_rate = rate_from_l2 - else: - logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price") - ticker = self.exchange.fetch_ticker(pair) - ticker_rate = ticker[bid_strategy['price_side']] - if ticker['last'] and ticker_rate > ticker['last']: - balance = bid_strategy['ask_last_balance'] - ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) - used_rate = ticker_rate - - self._buy_rate_cache[pair] = used_rate - - return used_rate def create_trade(self, pair: str) -> bool: """ @@ -532,7 +487,7 @@ class FreqtradeBot(LoggingMixin): buy_limit_requested = price else: # Calculate price - buy_limit_requested = self.get_buy_rate(pair, True) + buy_limit_requested = self.exchange.get_buy_rate(pair, True) if not buy_limit_requested: raise PricingError('Could not determine buy price.') @@ -657,7 +612,7 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a buy cancel occurred. """ - current_rate = self.get_buy_rate(trade.pair, False) + current_rate = self.exchange.get_buy_rate(trade.pair, False) msg = { 'trade_id': trade.id, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b6b395802..0e68d054d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1684,6 +1684,50 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name): exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50) +@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [ + ('ask', 20, 19, 10, 0.0, 20), # Full ask side + ('ask', 20, 19, 10, 1.0, 10), # Full last side + ('ask', 20, 19, 10, 0.5, 15), # Between ask and last + ('ask', 20, 19, 10, 0.7, 13), # Between ask and last + ('ask', 20, 19, 10, 0.3, 17), # Between ask and last + ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask + ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask + ('ask', 10, 20, None, 0.5, 10), # last not available - uses ask + ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask + ('ask', 4, 5, None, 1, 4), # last not available - uses ask + ('ask', 4, 5, None, 0, 4), # last not available - uses ask + ('bid', 21, 20, 10, 0.0, 20), # Full bid side + ('bid', 21, 20, 10, 1.0, 10), # Full last side + ('bid', 21, 20, 10, 0.5, 15), # Between bid and last + ('bid', 21, 20, 10, 0.7, 13), # Between bid and last + ('bid', 21, 20, 10, 0.3, 17), # Between bid and last + ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid + ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid + ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid + ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid + ('bid', 6, 5, None, 1, 5), # last not available - uses bid + ('bid', 6, 5, None, 0, 5), # last not available - uses bid +]) +def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, + last, last_ab, expected) -> None: + caplog.set_level(logging.DEBUG) + default_conf['bid_strategy']['ask_last_balance'] = last_ab + default_conf['bid_strategy']['price_side'] = side + exchange = get_patched_exchange(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': ask, 'last': last, 'bid': bid}) + + assert exchange.get_buy_rate('ETH/BTC', True) == expected + assert not log_has("Using cached buy rate for ETH/BTC.", caplog) + + assert exchange.get_buy_rate('ETH/BTC', False) == expected + assert log_has("Using cached buy rate for ETH/BTC.", caplog) + # Running a 2nd time with Refresh on! + caplog.clear() + assert exchange.get_buy_rate('ETH/BTC', True) == expected + assert not log_has("Using cached buy rate for ETH/BTC.", caplog) + + def make_fetch_ohlcv_mock(data): def fetch_ohlcv_mock(pair, timeframe, since): if since: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 4d9284a2f..68d861ef2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -751,49 +751,6 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0] -@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [ - ('ask', 20, 19, 10, 0.0, 20), # Full ask side - ('ask', 20, 19, 10, 1.0, 10), # Full last side - ('ask', 20, 19, 10, 0.5, 15), # Between ask and last - ('ask', 20, 19, 10, 0.7, 13), # Between ask and last - ('ask', 20, 19, 10, 0.3, 17), # Between ask and last - ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask - ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask - ('ask', 10, 20, None, 0.5, 10), # last not available - uses ask - ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask - ('ask', 4, 5, None, 1, 4), # last not available - uses ask - ('ask', 4, 5, None, 0, 4), # last not available - uses ask - ('bid', 21, 20, 10, 0.0, 20), # Full bid side - ('bid', 21, 20, 10, 1.0, 10), # Full last side - ('bid', 21, 20, 10, 0.5, 15), # Between bid and last - ('bid', 21, 20, 10, 0.7, 13), # Between bid and last - ('bid', 21, 20, 10, 0.3, 17), # Between bid and last - ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid - ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid - ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid - ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid - ('bid', 6, 5, None, 1, 5), # last not available - uses bid - ('bid', 6, 5, None, 0, 5), # last not available - uses bid -]) -def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, - last, last_ab, expected) -> None: - caplog.set_level(logging.DEBUG) - default_conf['bid_strategy']['ask_last_balance'] = last_ab - default_conf['bid_strategy']['price_side'] = side - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': ask, 'last': last, 'bid': bid}) - - assert freqtrade.get_buy_rate('ETH/BTC', True) == expected - assert not log_has("Using cached buy rate for ETH/BTC.", caplog) - - assert freqtrade.get_buy_rate('ETH/BTC', False) == expected - assert log_has("Using cached buy rate for ETH/BTC.", caplog) - # Running a 2nd time with Refresh on! - caplog.clear() - assert freqtrade.get_buy_rate('ETH/BTC', True) == expected - assert not log_has("Using cached buy rate for ETH/BTC.", caplog) - def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: patch_RPCManager(mocker) From bd1984386e1377b3e5dd297a4da60eab6ae55988 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Jun 2021 11:39:18 +0200 Subject: [PATCH 0606/1386] Move get_sell_rate to exchange class --- freqtrade/exchange/exchange.py | 53 ++++++++++++++++ freqtrade/freqtradebot.py | 71 +++------------------ freqtrade/rpc/rpc.py | 8 +-- tests/exchange/test_exchange.py | 104 ++++++++++++++++++++++++++++++- tests/rpc/test_rpc.py | 6 +- tests/rpc/test_rpc_apiserver.py | 2 +- tests/test_freqtradebot.py | 105 +------------------------------- 7 files changed, 172 insertions(+), 177 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 42b518566..67676d4e0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -88,6 +88,10 @@ class Exchange: # Cache for 10 minutes ... self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) + # Cache values for 1800 to avoid frequent polling of the exchange for prices + # Caching only applies to RPC methods, so prices for open trades are still + # refreshed once every iteration. + self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) # Holds candles @@ -912,6 +916,15 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def _order_book_gen(self, pair: str, side: str, order_book_max: int = 1, + order_book_min: int = 1): + """ + Helper generator to query orderbook in loop (used for early sell-order placing) + """ + order_book = self.fetch_l2_order_book(pair, order_book_max) + for i in range(order_book_min, order_book_max + 1): + yield order_book[side][i - 1][0] + def get_buy_rate(self, pair: str, refresh: bool) -> float: """ Calculates bid target between current ask price and last price @@ -958,6 +971,46 @@ class Exchange: return used_rate + def get_sell_rate(self, pair: str, refresh: bool) -> float: + """ + Get sell rate - either using ticker bid or first bid based on orderbook + or remain static in any other case since it's not updating. + :param pair: Pair to get rate for + :param refresh: allow cached data + :return: Bid rate + :raises PricingError if price could not be determined. + """ + if not refresh: + rate = self._sell_rate_cache.get(pair) + # Check if cache has been invalidated + if rate: + logger.debug(f"Using cached sell rate for {pair}.") + return rate + + ask_strategy = self._config.get('ask_strategy', {}) + if ask_strategy.get('use_order_book', False): + # This code is only used for notifications, selling uses the generator directly + logger.info( + f"Getting price from order book {ask_strategy['price_side'].capitalize()} side." + ) + try: + rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s")) + except (IndexError, KeyError) as e: + logger.warning("Sell Price at location from orderbook could not be determined.") + raise PricingError from e + else: + ticker = self.fetch_ticker(pair) + ticker_rate = ticker[ask_strategy['price_side']] + if ticker['last'] and ticker_rate < ticker['last']: + balance = ask_strategy.get('bid_last_balance', 0.0) + ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last']) + rate = ticker_rate + + if rate is None: + raise PricingError(f"Sell-Rate for {pair} was empty.") + self._sell_rate_cache[pair] = rate + return rate + # Fee handling @retrier diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d15748864..8628931b6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -10,7 +10,6 @@ from threading import Lock from typing import Any, Dict, List, Optional import arrow -from cachetools import TTLCache from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -58,11 +57,6 @@ class FreqtradeBot(LoggingMixin): # Init objects self.config = config - # Cache values for 1800 to avoid frequent polling of the exchange for prices - # Caching only applies to RPC methods, so prices for open trades are still - # refreshed once every iteration. - self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) - self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) # Check config consistency here since strategies can set certain options @@ -395,7 +389,6 @@ class FreqtradeBot(LoggingMixin): return trades_created - def create_trade(self, pair: str) -> bool: """ Check the implemented trading strategy for buy signals. @@ -678,56 +671,6 @@ class FreqtradeBot(LoggingMixin): return trades_closed - def _order_book_gen(self, pair: str, side: str, order_book_max: int = 1, - order_book_min: int = 1): - """ - Helper generator to query orderbook in loop (used for early sell-order placing) - """ - order_book = self.exchange.fetch_l2_order_book(pair, order_book_max) - for i in range(order_book_min, order_book_max + 1): - yield order_book[side][i - 1][0] - - def get_sell_rate(self, pair: str, refresh: bool) -> float: - """ - Get sell rate - either using ticker bid or first bid based on orderbook - The orderbook portion is only used for rpc messaging, which would otherwise fail - for BitMex (has no bid/ask in fetch_ticker) - or remain static in any other case since it's not updating. - :param pair: Pair to get rate for - :param refresh: allow cached data - :return: Bid rate - """ - if not refresh: - rate = self._sell_rate_cache.get(pair) - # Check if cache has been invalidated - if rate: - logger.debug(f"Using cached sell rate for {pair}.") - return rate - - ask_strategy = self.config.get('ask_strategy', {}) - if ask_strategy.get('use_order_book', False): - # This code is only used for notifications, selling uses the generator directly - logger.info( - f"Getting price from order book {ask_strategy['price_side'].capitalize()} side." - ) - try: - rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s")) - except (IndexError, KeyError) as e: - logger.warning("Sell Price at location from orderbook could not be determined.") - raise PricingError from e - else: - ticker = self.exchange.fetch_ticker(pair) - ticker_rate = ticker[ask_strategy['price_side']] - if ticker['last'] and ticker_rate < ticker['last']: - balance = ask_strategy.get('bid_last_balance', 0.0) - ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last']) - rate = ticker_rate - - if rate is None: - raise PricingError(f"Sell-Rate for {pair} was empty.") - self._sell_rate_cache[pair] = rate - return rate - def handle_trade(self, trade: Trade) -> bool: """ Sells the current pair if the threshold is reached and updates the trade record. @@ -755,9 +698,9 @@ class FreqtradeBot(LoggingMixin): logger.debug(f'Using order book between {order_book_min} and {order_book_max} ' f'for selling {trade.pair}...') - order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s", - order_book_min=order_book_min, - order_book_max=order_book_max) + order_book = self.exchange._order_book_gen( + trade.pair, f"{config_ask_strategy['price_side']}s", + order_book_min=order_book_min, order_book_max=order_book_max) for i in range(order_book_min, order_book_max + 1): try: sell_rate = next(order_book) @@ -770,14 +713,14 @@ class FreqtradeBot(LoggingMixin): f"{sell_rate:0.8f}") # Assign sell-rate to cache - otherwise sell-rate is never updated in the cache, # resulting in outdated RPC messages - self._sell_rate_cache[trade.pair] = sell_rate + self.exchange._sell_rate_cache[trade.pair] = sell_rate if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') - sell_rate = self.get_sell_rate(trade.pair, True) + sell_rate = self.exchange.get_sell_rate(trade.pair, True) if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True @@ -1209,7 +1152,7 @@ class FreqtradeBot(LoggingMixin): profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. - current_rate = self.get_sell_rate(trade.pair, False) if not fill else None + current_rate = self.exchange.get_sell_rate(trade.pair, False) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" @@ -1254,7 +1197,7 @@ class FreqtradeBot(LoggingMixin): profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) - current_rate = self.get_sell_rate(trade.pair, False) + current_rate = self.exchange.get_sell_rate(trade.pair, False) profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c609bccb8..40d5eb583 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -171,7 +171,7 @@ class RPC: # calculate profit and send message to user if trade.is_open: try: - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) except (ExchangeError, PricingError): current_rate = NAN else: @@ -230,7 +230,7 @@ class RPC: for trade in trades: # calculate profit and send message to user try: - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) except (PricingError, ExchangeError): current_rate = NAN trade_percent = (100 * trade.calc_profit_ratio(current_rate)) @@ -386,7 +386,7 @@ class RPC: else: # Get current rate try: - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) @@ -556,7 +556,7 @@ class RPC: if not fully_canceled: # Get current rate and execute sell - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0e68d054d..5fa94e6c1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,7 +11,7 @@ import pytest from pandas import DataFrame from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, - OperationalException, TemporaryError) + OperationalException, PricingError, TemporaryError) from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, calculate_backoff) @@ -1728,6 +1728,108 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, assert not log_has("Using cached buy rate for ETH/BTC.", caplog) +@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [ + ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side + ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side + ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat + ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid + ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid + ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid + ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), + ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side + ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side + ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat + ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask + ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask + ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask + ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), + ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), + ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), + ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), +]) +def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, + last, last_ab, expected) -> None: + caplog.set_level(logging.DEBUG) + + default_conf['ask_strategy']['price_side'] = side + default_conf['ask_strategy']['bid_last_balance'] = last_ab + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': ask, 'bid': bid, 'last': last}) + pair = "ETH/BTC" + + # Test regular mode + exchange = get_patched_exchange(mocker, default_conf) + rate = exchange.get_sell_rate(pair, True) + assert not log_has("Using cached sell rate for ETH/BTC.", caplog) + assert isinstance(rate, float) + assert rate == expected + # Use caching + rate = exchange.get_sell_rate(pair, False) + assert rate == expected + assert log_has("Using cached sell rate for ETH/BTC.", caplog) + + +@pytest.mark.parametrize('side,expected', [ + ('bid', 0.043936), # Value from order_book_l2 fiture - bids side + ('ask', 0.043949), # Value from order_book_l2 fiture - asks side +]) +def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, order_book_l2): + caplog.set_level(logging.DEBUG) + # Test orderbook mode + default_conf['ask_strategy']['price_side'] = side + default_conf['ask_strategy']['use_order_book'] = True + default_conf['ask_strategy']['order_book_min'] = 1 + default_conf['ask_strategy']['order_book_max'] = 2 + pair = "ETH/BTC" + mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) + exchange = get_patched_exchange(mocker, default_conf) + rate = exchange.get_sell_rate(pair, True) + assert not log_has("Using cached sell rate for ETH/BTC.", caplog) + assert isinstance(rate, float) + assert rate == expected + rate = exchange.get_sell_rate(pair, False) + assert rate == expected + assert log_has("Using cached sell rate for ETH/BTC.", caplog) + + +def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog): + # Test orderbook mode + default_conf['ask_strategy']['price_side'] = 'ask' + default_conf['ask_strategy']['use_order_book'] = True + default_conf['ask_strategy']['order_book_min'] = 1 + default_conf['ask_strategy']['order_book_max'] = 2 + pair = "ETH/BTC" + # Test What happens if the exchange returns an empty orderbook. + mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', + return_value={'bids': [[]], 'asks': [[]]}) + exchange = get_patched_exchange(mocker, default_conf) + with pytest.raises(PricingError): + exchange.get_sell_rate(pair, True) + assert log_has("Sell Price at location from orderbook could not be determined.", caplog) + + +def test_get_sell_rate_exception(default_conf, mocker, caplog): + # Ticker on one side can be empty in certain circumstances. + default_conf['ask_strategy']['price_side'] = 'ask' + pair = "ETH/BTC" + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': None, 'bid': 0.12, 'last': None}) + exchange = get_patched_exchange(mocker, default_conf) + with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): + exchange.get_sell_rate(pair, True) + + exchange._config['ask_strategy']['price_side'] = 'bid' + assert exchange.get_sell_rate(pair, True) == 0.12 + # Reverse sides + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': 0.13, 'bid': None, 'last': None}) + with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): + exchange.get_sell_rate(pair, True) + + exchange._config['ask_strategy']['price_side'] = 'ask' + assert exchange.get_sell_rate(pair, True) == 0.13 + + def make_fetch_ohlcv_mock(data): def fetch_ohlcv_mock(pair, timeframe, since): if since: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index e7a968e37..7556dde6d 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -109,7 +109,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'exchange': 'binance', } - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', + mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['current_profit']) @@ -217,7 +217,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.41% (-0.06)' == result[0][3] assert '-0.06' == f'{fiat_profit_sum:.2f}' - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', + mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] @@ -427,7 +427,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert prec_satoshi(stats['best_rate'], 6.2) # Test non-available pair - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', + mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert stats['trade_count'] == 2 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 1a66b2e81..def2e43c6 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -834,7 +834,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'exchange': 'binance', } - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', + mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) rc = client_get(client, f"{BASE_URI}/status") diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 68d861ef2..9039328b0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -751,7 +751,6 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0] - def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -2480,7 +2479,7 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: 'freqtrade.exchange.Exchange', cancel_order=cancel_order_mock, ) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', return_value=0.245441) + mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', return_value=0.245441) freqtrade = FreqtradeBot(default_conf) @@ -4029,108 +4028,6 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog) -@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [ - ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side - ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side - ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat - ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid - ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid - ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid - ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), - ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side - ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side - ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat - ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask - ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask - ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask - ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), - ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), - ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), - ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), -]) -def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, - last, last_ab, expected) -> None: - caplog.set_level(logging.DEBUG) - - default_conf['ask_strategy']['price_side'] = side - default_conf['ask_strategy']['bid_last_balance'] = last_ab - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': ask, 'bid': bid, 'last': last}) - pair = "ETH/BTC" - - # Test regular mode - ft = get_patched_freqtradebot(mocker, default_conf) - rate = ft.get_sell_rate(pair, True) - assert not log_has("Using cached sell rate for ETH/BTC.", caplog) - assert isinstance(rate, float) - assert rate == expected - # Use caching - rate = ft.get_sell_rate(pair, False) - assert rate == expected - assert log_has("Using cached sell rate for ETH/BTC.", caplog) - - -@pytest.mark.parametrize('side,expected', [ - ('bid', 0.043936), # Value from order_book_l2 fiture - bids side - ('ask', 0.043949), # Value from order_book_l2 fiture - asks side -]) -def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, order_book_l2): - caplog.set_level(logging.DEBUG) - # Test orderbook mode - default_conf['ask_strategy']['price_side'] = side - default_conf['ask_strategy']['use_order_book'] = True - default_conf['ask_strategy']['order_book_min'] = 1 - default_conf['ask_strategy']['order_book_max'] = 2 - pair = "ETH/BTC" - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) - ft = get_patched_freqtradebot(mocker, default_conf) - rate = ft.get_sell_rate(pair, True) - assert not log_has("Using cached sell rate for ETH/BTC.", caplog) - assert isinstance(rate, float) - assert rate == expected - rate = ft.get_sell_rate(pair, False) - assert rate == expected - assert log_has("Using cached sell rate for ETH/BTC.", caplog) - - -def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog): - # Test orderbook mode - default_conf['ask_strategy']['price_side'] = 'ask' - default_conf['ask_strategy']['use_order_book'] = True - default_conf['ask_strategy']['order_book_min'] = 1 - default_conf['ask_strategy']['order_book_max'] = 2 - pair = "ETH/BTC" - # Test What happens if the exchange returns an empty orderbook. - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', - return_value={'bids': [[]], 'asks': [[]]}) - ft = get_patched_freqtradebot(mocker, default_conf) - with pytest.raises(PricingError): - ft.get_sell_rate(pair, True) - assert log_has("Sell Price at location from orderbook could not be determined.", caplog) - - -def test_get_sell_rate_exception(default_conf, mocker, caplog): - # Ticker on one side can be empty in certain circumstances. - default_conf['ask_strategy']['price_side'] = 'ask' - pair = "ETH/BTC" - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': None, 'bid': 0.12, 'last': None}) - ft = get_patched_freqtradebot(mocker, default_conf) - with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): - ft.get_sell_rate(pair, True) - - ft.config['ask_strategy']['price_side'] = 'bid' - assert ft.get_sell_rate(pair, True) == 0.12 - # Reverse sides - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': 0.13, 'bid': None, 'last': None}) - with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): - ft.get_sell_rate(pair, True) - - ft.config['ask_strategy']['price_side'] = 'ask' - assert ft.get_sell_rate(pair, True) == 0.13 - - def test_startup_state(default_conf, mocker): default_conf['pairlist'] = {'method': 'VolumePairList', 'config': {'number_assets': 20} From 387f3bbc5d06b1123e03b586c7fcad4cce9d3868 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Jun 2021 11:43:47 +0200 Subject: [PATCH 0607/1386] Adjust missed tests --- tests/test_freqtradebot.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9039328b0..66866a8fc 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -759,13 +759,10 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order stake_amount = 2 bid = 0.11 buy_rate_mock = MagicMock(return_value=bid) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - get_buy_rate=buy_rate_mock, - ) buy_mm = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( 'freqtrade.exchange.Exchange', + get_buy_rate=buy_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, @@ -856,7 +853,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order assert not freqtrade.execute_buy(pair, stake_amount) # Fail to get price... - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_buy_rate', MagicMock(return_value=0.0)) + mocker.patch('freqtrade.exchange.Exchange.get_buy_rate', MagicMock(return_value=0.0)) with pytest.raises(PricingError, match="Could not determine buy price."): freqtrade.execute_buy(pair, stake_amount) @@ -864,10 +861,6 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - get_buy_rate=MagicMock(return_value=0.11), - ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ @@ -876,6 +869,7 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) - 'last': 0.00001172 }), buy=MagicMock(return_value=limit_buy_order), + get_buy_rate=MagicMock(return_value=0.11), get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) @@ -3934,7 +3928,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: default_conf['telegram']['enabled'] = False freqtrade = FreqtradeBot(default_conf) - assert freqtrade.get_buy_rate('ETH/BTC', True) == 0.043935 + assert freqtrade.exchange.get_buy_rate('ETH/BTC', True) == 0.043935 assert ticker_mock.call_count == 0 @@ -3956,7 +3950,7 @@ def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None freqtrade = FreqtradeBot(default_conf) # orderbook shall be used even if tickers would be lower. with pytest.raises(PricingError): - freqtrade.get_buy_rate('ETH/BTC', refresh=True) + freqtrade.exchange.get_buy_rate('ETH/BTC', refresh=True) assert log_has_re(r'Buy Price from orderbook could not be determined.', caplog) From cabab44b759934b528fb301023f7d17330131b43 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 May 2021 19:15:59 +0200 Subject: [PATCH 0608/1386] Combine docker build scripts --- .github/workflows/ci.yml | 14 ++--- .travis.yml | 6 -- ...lish_docker.sh => publish_docker_multi.sh} | 63 +++++++++++++------ build_helpers/publish_docker_pi.sh | 49 --------------- docker/Dockerfile.armhf | 6 +- 5 files changed, 52 insertions(+), 86 deletions(-) rename build_helpers/{publish_docker.sh => publish_docker_multi.sh} (54%) delete mode 100755 build_helpers/publish_docker_pi.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea766d77d..1ec0f22ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - master - stable - develop + - test_multiarch tags: release: types: [published] @@ -75,7 +76,7 @@ jobs: COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu run: | # Allow failure for coveralls - coveralls -v || true + coveralls || true - name: Backtesting run: | @@ -392,19 +393,12 @@ jobs: - name: Available platforms run: echo ${{ steps.buildx.outputs.platforms }} - - name: Build and test and push docker image + - name: Build and test and push docker images env: IMAGE_NAME: freqtradeorg/freqtrade BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} run: | - build_helpers/publish_docker.sh - - - name: Build Raspberry docker image - env: - IMAGE_NAME: freqtradeorg/freqtrade - BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} - run: | - build_helpers/publish_docker_pi.sh + build_helpers/publish_docker_multi.sh - name: Slack Notification diff --git a/.travis.yml b/.travis.yml index 03a8df49b..4535c44cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,12 +46,6 @@ jobs: - script: mypy freqtrade scripts name: mypy - # - stage: docker - # if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) - # script: - # - build_helpers/publish_docker.sh - # name: "Build and test and push docker image" - notifications: slack: secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q= diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker_multi.sh similarity index 54% rename from build_helpers/publish_docker.sh rename to build_helpers/publish_docker_multi.sh index da9fc4e34..a6b06ce7d 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker_multi.sh @@ -1,21 +1,48 @@ #!/bin/sh +# The below assumes a correctly setup docker buildx environment + # Replace / with _ to create a valid tag TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot +TAG_PI="${TAG}_pi" + +PI_PLATFORM="linux/arm/v7" echo "Running for ${TAG}" +CACHE_TAG=freqtradeorg/freqtrade_cache:${TAG}_cache # Add commit and commit_message to docker container echo "${GITHUB_SHA}" > freqtrade_commit if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache" + # Build regular image docker build -t freqtrade:${TAG} . + # Build PI image + docker buildx build \ + --cache-to=type=registry,ref=${CACHE_TAG} \ + -f docker/Dockerfile.armhf \ + --platform ${PI_PLATFORM} \ + -t ${IMAGE_NAME}:${TAG_PI} --push . else echo "event ${GITHUB_EVENT_NAME}: building with cache" - # Pull last build to avoid rebuilding the whole image + # Build regular image docker pull ${IMAGE_NAME}:${TAG} docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . + + # Pull last build to avoid rebuilding the whole image + # docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG} + docker buildx build \ + --cache-from=type=registry,ref=${CACHE_TAG} \ + --cache-to=type=registry,ref=${CACHE_TAG} \ + -f docker/Dockerfile.armhf \ + --platform ${PI_PLATFORM} \ + -t ${IMAGE_NAME}:${TAG_PI} --push . +fi + +if [ $? -ne 0 ]; then + echo "failed building multiarch images" + return 1 fi # Tag image for upload and next build step docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG @@ -24,11 +51,6 @@ docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${TAG} -t fre docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT -if [ $? -ne 0 ]; then - echo "failed building image" - return 1 -fi - # Run backtest docker run --rm -v $(pwd)/config_bittrex.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy @@ -37,24 +59,29 @@ if [ $? -ne 0 ]; then return 1 fi -if [ $? -ne 0 ]; then - echo "failed tagging image" - return 1 -fi - -# Tag as latest for develop builds -if [ "${TAG}" = "develop" ]; then - docker tag freqtrade:$TAG ${IMAGE_NAME}:latest -fi - -# Show all available images docker images docker push ${IMAGE_NAME} docker push ${IMAGE_NAME}:$TAG_PLOT docker push ${IMAGE_NAME}:$TAG +# Create multiarch image +# Make sure that all images contained here are pushed to github first. +# Otherwise installation might fail. + +docker manifest create freqtradeorg/freqtrade:${TAG} ${IMAGE_NAME}:${TAG} ${IMAGE_NAME}:${TAG_PI} +docker manifest push freqtradeorg/freqtrade:${TAG} + +# Tag as latest for develop builds +if [ "${TAG}" = "develop" ]; then + docker manifest create freqtradeorg/freqtrade:latest ${IMAGE_NAME}:${TAG} ${IMAGE_NAME}:${TAG_PI} + docker manifest push freqtradeorg/freqtrade:latest +fi + + +docker images + if [ $? -ne 0 ]; then - echo "failed pushing repo" + echo "failed building image" return 1 fi diff --git a/build_helpers/publish_docker_pi.sh b/build_helpers/publish_docker_pi.sh deleted file mode 100755 index c7024828b..000000000 --- a/build_helpers/publish_docker_pi.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh - -# The below assumes a correctly setup docker buildx environment - -# Replace / with _ to create a valid tag -TAG_ORIG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") -TAG="${TAG_ORIG}_pi" - -PI_PLATFORM="linux/arm/v7" -echo "Running for ${TAG}" -CACHE_TAG=freqtradeorg/freqtrade_cache:${TAG}_cache - -# Add commit and commit_message to docker container -echo "${GITHUB_SHA}" > freqtrade_commit - -if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then - echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache" - docker buildx build \ - --cache-to=type=registry,ref=${CACHE_TAG} \ - -f docker/Dockerfile.armhf \ - --platform ${PI_PLATFORM} \ - -t ${IMAGE_NAME}:${TAG} --push . -else - echo "event ${GITHUB_EVENT_NAME}: building with cache" - # Pull last build to avoid rebuilding the whole image - # docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG} - docker buildx build \ - --cache-from=type=registry,ref=${CACHE_TAG} \ - --cache-to=type=registry,ref=${CACHE_TAG} \ - -f docker/Dockerfile.armhf \ - --platform ${PI_PLATFORM} \ - -t ${IMAGE_NAME}:${TAG} --push . -fi - -docker images - -# Create multiarch image -# Make sure that all images contained here are pushed to github first. -# Otherwise installation might fail. - -docker manifest create freqtradeorg/freqtrade:${TAG_ORIG} ${IMAGE_NAME}:${TAG_ORIG} ${IMAGE_NAME}:${TAG} -docker manifest push freqtradeorg/freqtrade:${TAG_ORIG} - -docker images - -if [ $? -ne 0 ]; then - echo "failed building image" - return 1 -fi diff --git a/docker/Dockerfile.armhf b/docker/Dockerfile.armhf index 8abf0e44b..f9827774e 100644 --- a/docker/Dockerfile.armhf +++ b/docker/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base +FROM python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 @@ -11,7 +11,7 @@ ENV FT_APP_ENV="docker" # Prepare environment RUN mkdir /freqtrade \ && apt-get update \ - && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \ + && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-dev \ && apt-get clean \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ @@ -23,7 +23,7 @@ WORKDIR /freqtrade # Install dependencies FROM base as python-deps RUN apt-get update \ - && apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 \ + && apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf From 80af6e43e4f94cc442f8341a0ed1c1b7e691ff22 Mon Sep 17 00:00:00 2001 From: Janos Date: Wed, 2 Jun 2021 20:50:08 +0200 Subject: [PATCH 0609/1386] test-pairlist: remove non-JSON headline from JSON output --- freqtrade/commands/pairlist_commands.py | 2 +- tests/commands/test_commands.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index 0661cd03c..a2b10a42f 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -31,7 +31,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: results[curr] = pairlists.whitelist for curr, pairlist in results.items(): - if not args.get('print_one_column', False): + if not args.get('print_one_column', False) and not args.get('list_pairs_print_json', False): print(f"Pairs for {curr}: ") if args.get('print_one_column', False): diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 71ae0ed78..b0808ae06 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,3 +1,4 @@ +import json import re from io import BytesIO from pathlib import Path @@ -914,8 +915,15 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): ] start_test_pairlist(get_args(args)) captured = capsys.readouterr() - assert re.match(r'Pairs for BTC: \n\["ETH/BTC","TKN/BTC","BLK/BTC","LTC/BTC","XRP/BTC"\]\n', - captured.out) + try: + json_pairs = json.loads(captured.out) + assert 'ETH/BTC' in json_pairs + assert 'TKN/BTC' in json_pairs + assert 'BLK/BTC' in json_pairs + assert 'LTC/BTC' in json_pairs + assert 'XRP/BTC' in json_pairs + except json.decoder.JSONDecodeError: + pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}') def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, From 812eb229df04db2461fdccc49d2f577bf64f8ab2 Mon Sep 17 00:00:00 2001 From: Janos Date: Sun, 30 May 2021 16:11:24 +0200 Subject: [PATCH 0610/1386] plot-profit: Make "auto-open" HTML result optional Adding an "--auto-open" argument. This improves tool processing of the results, while still allowing to open the HTML file for easy use. --- docs/plotting.md | 1 + freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 5 +++++ freqtrade/configuration/configuration.py | 3 +++ freqtrade/plot/plotting.py | 3 ++- tests/test_arguments.py | 6 +++++- tests/test_plotting.py | 2 +- 7 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 05708ce66..9fae38504 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -275,6 +275,7 @@ optional arguments: (backtest file)) Default: file -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). + --auto-open Automatically open generated plot. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index ffd317799..7f4f7edd6 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -69,7 +69,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "timerange", "timeframe", "no_trades"] ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", - "trade_source", "timeframe"] + "trade_source", "timeframe", "plot_auto_open"] ARGS_INSTALL_UI = ["erase_ui_only"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index b583b47ba..d832693ee 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -433,6 +433,11 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=750, ), + "plot_auto_open": Arg( + '--auto-open', + help='Automatically open generated plot.', + action='store_true', + ), "no_trades": Arg( '--no-trades', help='Skip using trades from backtesting file and DB.', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index f6d0520c5..abb9f994b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -375,6 +375,9 @@ class Configuration: self._args_to_config(config, argname='plot_limit', logstring='Limiting plot to: {}') + self._args_to_config(config, argname='plot_auto_open', + logstring='Parameter --auto-open detected.') + self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index b62ae6015..f52b0bc81 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -602,4 +602,5 @@ def plot_profit(config: Dict[str, Any]) -> None: trades, config.get('timeframe', '5m'), config.get('stake_currency', '')) store_plot_file(fig, filename='freqtrade-profit-plot.html', - directory=config['user_data_dir'] / 'plot', auto_open=True) + directory=config['user_data_dir'] / 'plot', + auto_open=config.get('plot_auto_open', False)) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 60c2cfbac..0d81dea28 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -186,18 +186,22 @@ def test_plot_dataframe_options() -> None: assert pargs['pairs'] == ['UNITTEST/BTC'] -def test_plot_profit_options() -> None: +@pytest.mark.parametrize('auto_open_arg', [True, False]) +def test_plot_profit_options(auto_open_arg: bool) -> None: args = [ 'plot-profit', '-p', 'UNITTEST/BTC', '--trade-source', 'DB', '--db-url', 'sqlite:///whatever.sqlite', ] + if auto_open_arg: + args.append('--auto-open') pargs = Arguments(args).get_parsed_arg() assert pargs['trade_source'] == 'DB' assert pargs['pairs'] == ['UNITTEST/BTC'] assert pargs['db_url'] == 'sqlite:///whatever.sqlite' + assert pargs['plot_auto_open'] == auto_open_arg def test_config_notallowed(mocker) -> None: diff --git a/tests/test_plotting.py b/tests/test_plotting.py index a22c8c681..0b1054ced 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -460,7 +460,7 @@ def test_plot_profit(default_conf, mocker, testdatadir): assert store_mock.call_count == 1 assert profit_mock.call_args_list[0][0][0] == default_conf['pairs'] - assert store_mock.call_args_list[0][1]['auto_open'] is True + assert store_mock.call_args_list[0][1]['auto_open'] is False @pytest.mark.parametrize("ind1,ind2,plot_conf,exp", [ From 42b6d28b3ca0bab08a4d37e5cafa302d8d86e69f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Jun 2021 19:20:04 +0200 Subject: [PATCH 0611/1386] Update warning about order_time_in_force as pointed out in #3009 --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7fa751bef..ef6f34094 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -406,8 +406,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`. ``` !!! Warning - This is an ongoing work. For now it is supported only for binance and only for buy orders. - Please don't change the default value unless you know what you are doing. + This is an ongoing work. For now it is supported only for binance. + Please don't change the default value unless you know what you are doing and have researched the impact of using different values. ### Exchange configuration From a0893b291a099b928ced567b9ef81f2d2795717a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Jun 2021 09:03:03 +0200 Subject: [PATCH 0612/1386] Fix strategy samples to use runmode.value closes #5073 --- freqtrade/templates/sample_strategy.py | 2 +- freqtrade/templates/subtemplates/indicators_full.j2 | 2 +- freqtrade/templates/subtemplates/indicators_minimal.j2 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index e51feff1e..282b2f8e2 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -329,7 +329,7 @@ class SampleStrategy(IStrategy): """ # first check if dataprovider is available if self.dp: - if self.dp.runmode in ('live', 'dry_run'): + if self.dp.runmode.value in ('live', 'dry_run'): ob = self.dp.orderbook(metadata['pair'], 1) dataframe['best_bid'] = ob['bids'][0][0] dataframe['best_ask'] = ob['asks'][0][0] diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 index 57d2ca665..a497b47cb 100644 --- a/freqtrade/templates/subtemplates/indicators_full.j2 +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -199,7 +199,7 @@ dataframe['htleadsine'] = hilbert['leadsine'] """ # first check if dataprovider is available if self.dp: - if self.dp.runmode in ('live', 'dry_run'): + if self.dp.runmode.value in ('live', 'dry_run'): ob = self.dp.orderbook(metadata['pair'], 1) dataframe['best_bid'] = ob['bids'][0][0] dataframe['best_ask'] = ob['asks'][0][0] diff --git a/freqtrade/templates/subtemplates/indicators_minimal.j2 b/freqtrade/templates/subtemplates/indicators_minimal.j2 index 7d75b4610..90f4f4d4a 100644 --- a/freqtrade/templates/subtemplates/indicators_minimal.j2 +++ b/freqtrade/templates/subtemplates/indicators_minimal.j2 @@ -10,7 +10,7 @@ dataframe['rsi'] = ta.RSI(dataframe) """ # first check if dataprovider is available if self.dp: - if self.dp.runmode in ('live', 'dry_run'): + if self.dp.runmode.value in ('live', 'dry_run'): ob = self.dp.orderbook(metadata['pair'], 1) dataframe['best_bid'] = ob['bids'][0][0] dataframe['best_ask'] = ob['asks'][0][0] From 1e988c97ad1fc3a25ee4a40834a4795447ae370a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Jun 2021 20:55:18 +0200 Subject: [PATCH 0613/1386] Update dry-run order handling to use realistic fill prices closes #3389 --- freqtrade/exchange/exchange.py | 93 ++++++++++++++++++++++++++------- tests/exchange/test_exchange.py | 1 + tests/rpc/test_rpc_apiserver.py | 3 +- tests/rpc/test_rpc_telegram.py | 4 ++ tests/test_freqtradebot.py | 2 + 5 files changed, 84 insertions(+), 19 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 67676d4e0..87798e612 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -561,7 +561,7 @@ class Exchange: rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) - dry_order = { + dry_order: Dict[str, Any] = { 'id': order_id, 'symbol': pair, 'price': rate, @@ -577,26 +577,73 @@ class Exchange: 'fee': None, 'info': {} } - self._store_dry_order(dry_order, pair) + if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: + dry_order["info"] = {"stopPrice": dry_order["price"]} + + if dry_order["type"] == "market": + # Update market order pricing + average = self.get_dry_market_fill_price(pair, side, amount, rate) + dry_order.update({ + 'average': average, + 'cost': dry_order['amount'] * average, + }) + self.add_dry_order_fee(pair, dry_order) + + self._dry_run_open_orders[dry_order["id"]] = dry_order # Copy order and close it - so the returned order is open unless it's a market order return dry_order - def _store_dry_order(self, dry_order: Dict, pair: str) -> None: - closed_order = dry_order.copy() - if closed_order['type'] in ["market", "limit"]: - closed_order.update({ - 'status': 'closed', - 'filled': closed_order['amount'], - 'remaining': 0, - 'fee': { - 'currency': self.get_pair_quote_currency(pair), - 'cost': dry_order['cost'] * self.get_fee(pair), - 'rate': self.get_fee(pair) - } - }) - if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: - closed_order["info"].update({"stopPrice": closed_order["price"]}) - self._dry_run_open_orders[closed_order["id"]] = closed_order + def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]): + dry_order.update({ + 'fee': { + 'currency': self.get_pair_quote_currency(pair), + 'cost': dry_order['cost'] * self.get_fee(pair), + 'rate': self.get_fee(pair) + } + }) + + def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float: + """ + Get the market order fill price based on orderbook interpolation + """ + if self.exchange_has('fetchL2OrderBook'): + ob = self.fetch_l2_order_book(pair, 20) + book_entry_type = 'asks' if side == 'buy' else 'bids' + + remaining_amount = amount + filled_amount = 0 + for book_entry in ob[book_entry_type]: + book_entry_price = book_entry[0] + book_entry_coin_volume = book_entry[1] + book_entry_ref_currency_volume = book_entry_price * book_entry_coin_volume + if remaining_amount > 0: + if remaining_amount < book_entry_ref_currency_volume: + filled_amount += remaining_amount * book_entry_price + else: + filled_amount += book_entry_ref_currency_volume * book_entry_price + remaining_amount -= book_entry_ref_currency_volume + else: + break + forecast_avg_filled_price = filled_amount / amount + return self.price_to_precision(pair, forecast_avg_filled_price) + + return rate + + def dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool: + if not self.exchange_has('fetchL2OrderBook'): + return True + ob = self.fetch_l2_order_book(pair, 1) + if side == 'buy': + price = ob['asks'][0][0] + logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}") + if limit >= price: + return True + else: + price = ob['bids'][0][0] + logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}") + if limit <= price: + return True + return False def fetch_dry_run_order(self, order_id) -> Dict[str, Any]: """ @@ -605,6 +652,16 @@ class Exchange: """ try: order = self._dry_run_open_orders[order_id] + pair = order['symbol'] + if order['status'] != "closed" and order['type'] in ["limit"]: + if self.dry_limit_order_filled(pair, order['side'], order['price']): + order.update({ + 'status': 'closed', + 'filled': order['amount'], + 'remaining': 0, + }) + self.add_dry_order_fee(pair, order) + return order except KeyError as e: # Gracefully handle errors with dry-run orders. diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5fa94e6c1..73b8022d1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2117,6 +2117,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange def test_cancel_order_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + mocker.patch('freqtrade.exchange.Exchange.dry_limit_order_filled', return_value=True) assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index def2e43c6..f47819568 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -996,7 +996,8 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) + markets=PropertyMock(return_value=markets), + dry_limit_order_filled=MagicMock(return_value=True), ) patch_get_signal(ftbot, (True, False)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 4c60bdad3..50c8a36ce 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -225,6 +225,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + dry_limit_order_filled=MagicMock(return_value=True), ) status_table = MagicMock() mocker.patch.multiple( @@ -671,6 +672,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + dry_limit_order_filled=MagicMock(return_value=True), ) freqtradebot = FreqtradeBot(default_conf) @@ -729,6 +731,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + dry_limit_order_filled=MagicMock(return_value=True), ) freqtradebot = FreqtradeBot(default_conf) @@ -789,6 +792,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + dry_limit_order_filled=MagicMock(return_value=True), ) default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 66866a8fc..0866deead 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2750,6 +2750,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke price_to_precision=lambda s, x, y: y, stoploss=stoploss, cancel_stoploss_order=cancel_order, + dry_limit_order_filled=MagicMock(return_value=True), ) freqtrade = FreqtradeBot(default_conf) @@ -2792,6 +2793,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, + dry_limit_order_filled=MagicMock(return_value=True), ) stoploss = MagicMock(return_value={ From db03a2410958fff66dd943f35bc46527976876f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Jun 2021 06:44:51 +0200 Subject: [PATCH 0614/1386] Add tests for fill methods --- freqtrade/exchange/exchange.py | 14 ++++---- tests/conftest.py | 34 ++++++++++++++++++ tests/exchange/test_exchange.py | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 87798e612..ea3a7d7cd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -608,22 +608,24 @@ class Exchange: """ if self.exchange_has('fetchL2OrderBook'): ob = self.fetch_l2_order_book(pair, 20) - book_entry_type = 'asks' if side == 'buy' else 'bids' + ob_type = 'asks' if side == 'buy' else 'bids' remaining_amount = amount filled_amount = 0 - for book_entry in ob[book_entry_type]: + for book_entry in ob[ob_type]: book_entry_price = book_entry[0] book_entry_coin_volume = book_entry[1] - book_entry_ref_currency_volume = book_entry_price * book_entry_coin_volume if remaining_amount > 0: - if remaining_amount < book_entry_ref_currency_volume: + if remaining_amount < book_entry_coin_volume: filled_amount += remaining_amount * book_entry_price else: - filled_amount += book_entry_ref_currency_volume * book_entry_price - remaining_amount -= book_entry_ref_currency_volume + filled_amount += book_entry_coin_volume * book_entry_price + remaining_amount -= book_entry_coin_volume else: break + else: + # If remaining_amount wasn't consumed completely (break was not called) + filled_amount += remaining_amount * book_entry_price forecast_avg_filled_price = filled_amount / amount return self.price_to_precision(pair, forecast_avg_filled_price) diff --git a/tests/conftest.py b/tests/conftest.py index 43a98647f..8ce41cf9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1087,6 +1087,40 @@ def order_book_l2(): }) +@pytest.fixture +def order_book_l2_usd(): + return MagicMock(return_value={ + 'symbol': 'LTC/USDT', + 'bids': [ + [25.563, 49.269], + [25.562, 83.0], + [25.56, 106.0], + [25.559, 15.381], + [25.558, 29.299], + [25.557, 34.624], + [25.556, 10.0], + [25.555, 14.684], + [25.554, 45.91], + [25.553, 50.0] + ], + 'asks': [ + [25.566, 14.27], + [25.567, 48.484], + [25.568, 92.349], + [25.572, 31.48], + [25.573, 23.0], + [25.574, 20.0], + [25.575, 89.606], + [25.576, 262.016], + [25.577, 178.557], + [25.578, 78.614] + ], + 'timestamp': None, + 'datetime': None, + 'nonce': 2372149736 + }) + + @pytest.fixture def ohlcv_history_list(): return [ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 73b8022d1..2c4ddacb4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -947,6 +947,70 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name): assert order["symbol"] == "ETH/BTC" +@pytest.mark.parametrize("side,startprice,endprice", [ + ("buy", 25.563, 25.566), + ("sell", 25.566, 25.563) +]) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, endprice, + exchange_name, order_book_l2_usd): + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + mocker.patch.multiple('freqtrade.exchange.Exchange', + exchange_has=MagicMock(return_value=True), + fetch_l2_order_book=order_book_l2_usd, + ) + + order = exchange.create_dry_run_order( + pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice) + assert 'id' in order + assert f'dry_run_{side}_' in order["id"] + assert order["side"] == side + assert order["type"] == "limit" + assert order["symbol"] == "LTC/USDT" + + order_closed = exchange.fetch_dry_run_order(order['id']) + assert order_book_l2_usd.call_count == 1 + assert order_closed['status'] == 'open' + assert not order['fee'] + + order_book_l2_usd.reset_mock() + order_closed['price'] = endprice + + order_closed = exchange.fetch_dry_run_order(order['id']) + assert order_closed['status'] == 'closed' + assert order['fee'] + + +@pytest.mark.parametrize("side,amount,endprice", [ + ("buy", 1, 25.566), + ("buy", 100, 25.5672), # Requires interpolation + ("buy", 1000, 25.575), # More than orderbook return + ("sell", 1, 25.563), + ("sell", 100, 25.5625), # Requires interpolation + ("sell", 1000, 25.5555), # More than orderbook return +]) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, endprice, + exchange_name, order_book_l2_usd): + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + mocker.patch.multiple('freqtrade.exchange.Exchange', + exchange_has=MagicMock(return_value=True), + fetch_l2_order_book=order_book_l2_usd, + ) + + order = exchange.create_dry_run_order( + pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=25.5) + assert 'id' in order + assert f'dry_run_{side}_' in order["id"] + assert order["side"] == side + assert order["type"] == "market" + assert order["symbol"] == "LTC/USDT" + assert order['status'] == 'closed' + assert round(order["average"], 4) == round(endprice, 4) + + @pytest.mark.parametrize("side", [ ("buy"), ("sell") From c389d44e9ac35cdf1f3c4a903f8d89ca8bb1b6f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Jun 2021 15:22:52 +0200 Subject: [PATCH 0615/1386] Improve filling logic --- freqtrade/exchange/exchange.py | 36 +++++++++++++++++++++------------ tests/exchange/test_exchange.py | 4 +++- tests/rpc/test_rpc.py | 15 ++++++++------ tests/rpc/test_rpc_apiserver.py | 2 +- tests/rpc/test_rpc_telegram.py | 14 ++++++------- tests/test_freqtradebot.py | 11 ++++++++-- 6 files changed, 52 insertions(+), 30 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea3a7d7cd..19b646a93 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -587,13 +587,15 @@ class Exchange: 'average': average, 'cost': dry_order['amount'] * average, }) - self.add_dry_order_fee(pair, dry_order) + dry_order = self.add_dry_order_fee(pair, dry_order) + + dry_order = self.check_dry_limit_order_filled(dry_order) self._dry_run_open_orders[dry_order["id"]] = dry_order # Copy order and close it - so the returned order is open unless it's a market order return dry_order - def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]): + def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]: dry_order.update({ 'fee': { 'currency': self.get_pair_quote_currency(pair), @@ -601,6 +603,7 @@ class Exchange: 'rate': self.get_fee(pair) } }) + return dry_order def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float: """ @@ -631,7 +634,7 @@ class Exchange: return rate - def dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool: + def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool: if not self.exchange_has('fetchL2OrderBook'): return True ob = self.fetch_l2_order_book(pair, 1) @@ -647,6 +650,22 @@ class Exchange: return True return False + def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]: + """ + Check dry-run limit order fill and update fee (if it filled). + """ + if order['status'] != "closed" and order['type'] in ["limit"]: + pair = order['symbol'] + if self._is_dry_limit_order_filled(pair, order['side'], order['price']): + order.update({ + 'status': 'closed', + 'filled': order['amount'], + 'remaining': 0, + }) + self.add_dry_order_fee(pair, order) + + return order + def fetch_dry_run_order(self, order_id) -> Dict[str, Any]: """ Return dry-run order @@ -654,16 +673,7 @@ class Exchange: """ try: order = self._dry_run_open_orders[order_id] - pair = order['symbol'] - if order['status'] != "closed" and order['type'] in ["limit"]: - if self.dry_limit_order_filled(pair, order['side'], order['price']): - order.update({ - 'status': 'closed', - 'filled': order['amount'], - 'remaining': 0, - }) - self.add_dry_order_fee(pair, order) - + order = self.check_dry_limit_order_filled(order) return order except KeyError as e: # Gracefully handle errors with dry-run orders. diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 2c4ddacb4..42bb07175 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -963,11 +963,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, order = exchange.create_dry_run_order( pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice) + assert order_book_l2_usd.call_count == 1 assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side assert order["type"] == "limit" assert order["symbol"] == "LTC/USDT" + order_book_l2_usd.reset_mock() order_closed = exchange.fetch_dry_run_order(order['id']) assert order_book_l2_usd.call_count == 1 @@ -2181,7 +2183,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange def test_cancel_order_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - mocker.patch('freqtrade.exchange.Exchange.dry_limit_order_filled', return_value=True) + mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True) assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 7556dde6d..fd231b614 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -679,6 +679,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: 'filled': 0.0, } ), + _is_dry_limit_order_filled=MagicMock(return_value=True), get_fee=fee, ) mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000) @@ -703,8 +704,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert msg == {'result': 'Created sell orders for all open trades.'} freqtradebot.enter_positions() - msg = rpc._rpc_forcesell('1') - assert msg == {'result': 'Created sell order for trade 1.'} + msg = rpc._rpc_forcesell('2') + assert msg == {'result': 'Created sell order for trade 2.'} freqtradebot.state = State.STOPPED with pytest.raises(RPCException, match=r'.*trader is not running*'): @@ -715,9 +716,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING assert cancel_order_mock.call_count == 0 + mocker.patch( + 'freqtrade.exchange.Exchange._is_dry_limit_order_filled', MagicMock(return_value=False)) freqtradebot.enter_positions() # make an limit-buy open trade - trade = Trade.query.filter(Trade.id == '1').first() + trade = Trade.query.filter(Trade.id == '3').first() filled_amount = trade.amount / 2 # Fetch order - it's open first, and closed after cancel_order is called. mocker.patch( @@ -738,7 +741,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: ) # check that the trade is called, which is done by ensuring exchange.cancel_order is called # and trade amount is updated - rpc._rpc_forcesell('1') + rpc._rpc_forcesell('3') assert cancel_order_mock.call_count == 1 assert trade.amount == filled_amount @@ -766,8 +769,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: } ) # check that the trade is called, which is done by ensuring exchange.cancel_order is called - msg = rpc._rpc_forcesell('2') - assert msg == {'result': 'Created sell order for trade 2.'} + msg = rpc._rpc_forcesell('4') + assert msg == {'result': 'Created sell order for trade 4.'} assert cancel_order_mock.call_count == 2 assert trade.amount == amount diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f47819568..f30825b7b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -997,7 +997,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets), - dry_limit_order_filled=MagicMock(return_value=True), + _is_dry_limit_order_filled=MagicMock(return_value=False), ) patch_get_signal(ftbot, (True, False)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 50c8a36ce..c933ac648 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -225,7 +225,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - dry_limit_order_filled=MagicMock(return_value=True), + _is_dry_limit_order_filled=MagicMock(return_value=True), ) status_table = MagicMock() mocker.patch.multiple( @@ -672,7 +672,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - dry_limit_order_filled=MagicMock(return_value=True), + _is_dry_limit_order_filled=MagicMock(return_value=True), ) freqtradebot = FreqtradeBot(default_conf) @@ -731,7 +731,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - dry_limit_order_filled=MagicMock(return_value=True), + _is_dry_limit_order_filled=MagicMock(return_value=True), ) freqtradebot = FreqtradeBot(default_conf) @@ -792,7 +792,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - dry_limit_order_filled=MagicMock(return_value=True), + _is_dry_limit_order_filled=MagicMock(return_value=True), ) default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) @@ -809,9 +809,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - # Called for each trade 4 times - assert msg_mock.call_count == 12 - msg = msg_mock.call_args_list[2][0][0] + # Called for each trade 2 times + assert msg_mock.call_count == 8 + msg = msg_mock.call_args_list[1][0][0] assert { 'type': RPCMessageType.SELL, 'trade_id': 1, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0866deead..4bcc578c4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -305,6 +305,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -334,6 +335,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) # Save state of current whitelist @@ -2533,6 +2535,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) @@ -2596,6 +2599,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) @@ -2648,6 +2652,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) @@ -2750,7 +2755,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke price_to_precision=lambda s, x, y: y, stoploss=stoploss, cancel_stoploss_order=cancel_order, - dry_limit_order_filled=MagicMock(return_value=True), + _is_dry_limit_order_filled=MagicMock(side_effect=[True, False]), ) freqtrade = FreqtradeBot(default_conf) @@ -2793,7 +2798,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, - dry_limit_order_filled=MagicMock(return_value=True), + _is_dry_limit_order_filled=MagicMock(side_effect=[False, True]), ) stoploss = MagicMock(return_value={ @@ -2862,6 +2867,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) @@ -3467,6 +3473,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b }), buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) default_conf['ask_strategy'] = { 'ignore_roi_if_buy_signal': False From c76848e089ad722aa5f851d6da0a9823621f1ad5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Jun 2021 13:51:42 +0200 Subject: [PATCH 0616/1386] Update dry-run description with new filling logic --- docs/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index ef6f34094..63f55505a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -503,7 +503,8 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo * API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in dry-run mode. * Wallets (`/balance`) are simulated based on `dry_run_wallet`. * Orders are simulated, and will not be posted to the exchange. -* Orders are assumed to fill immediately, and will never time out. +* Market orders fill based on orderbook volume the moment the order is placed. +* Limit orders fill once price reaches the defined level - or time out based on `unfilledtimeout` settings. * In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled. * Open orders (not trades, which are stored in the database) are reset on bot restart. From 6479217cb485e39dcee1142c707ebf8436e9f786 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Jun 2021 14:16:32 +0200 Subject: [PATCH 0617/1386] Don't build for test_multiarch --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ec0f22ae..42959c3b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: - master - stable - develop - - test_multiarch tags: release: types: [published] From c8accd314a0aa8fce5c3edb67dc89c2782dc8e4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 05:21:47 +0000 Subject: [PATCH 0618/1386] Bump uvicorn from 0.13.4 to 0.14.0 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.13.4 to 0.14.0. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.13.4...0.14.0) --- updated-dependencies: - dependency-name: uvicorn 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 8f4fed9ad..3ee290d2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ sdnotify==0.3.2 # API Server fastapi==0.65.1 -uvicorn==0.13.4 +uvicorn==0.14.0 pyjwt==2.1.0 aiofiles==0.7.0 From 9073a053282b526954b6732bbcf484206be21d64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 05:21:53 +0000 Subject: [PATCH 0619/1386] Bump pytest-cov from 2.12.0 to 2.12.1 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.12.0 to 2.12.1. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.12.0...v2.12.1) --- updated-dependencies: - dependency-name: pytest-cov 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 7b106ae9b..6fbe581a5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ flake8-tidy-imports==4.3.0 mypy==0.812 pytest==6.2.4 pytest-asyncio==0.15.1 -pytest-cov==2.12.0 +pytest-cov==2.12.1 pytest-mock==3.6.1 pytest-random-order==1.0.4 isort==5.8.0 From 69d74544aafafd5c48fc21913bb04243072abd27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 05:22:00 +0000 Subject: [PATCH 0620/1386] Bump python-telegram-bot from 13.5 to 13.6 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.5 to 13.6. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.5...v13.6) --- 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 8f4fed9ad..a232de96b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.50.70 cryptography==3.4.7 aiohttp==3.7.4.post0 SQLAlchemy==1.4.17 -python-telegram-bot==13.5 +python-telegram-bot==13.6 arrow==1.1.0 cachetools==4.2.2 requests==2.25.1 From 2468ae35cda1d3a4e56a2458008de01e86e84d23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 05:22:09 +0000 Subject: [PATCH 0621/1386] Bump ccxt from 1.50.70 to 1.51.3 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.50.70 to 1.51.3. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.50.70...1.51.3) --- 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 8f4fed9ad..e10494b2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.3 pandas==1.2.4 -ccxt==1.50.70 +ccxt==1.51.3 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From 77a2feeb9f7a1ea0c1153512297a43fb5bf2c1dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 05:22:24 +0000 Subject: [PATCH 0622/1386] Bump pycoingecko from 2.0.0 to 2.1.0 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 2.0.0 to 2.1.0. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/2.0.0...2.1.0) --- updated-dependencies: - dependency-name: pycoingecko 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 8f4fed9ad..5a857b605 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonschema==3.2.0 TA-Lib==0.4.20 technical==1.3.0 tabulate==0.8.9 -pycoingecko==2.0.0 +pycoingecko==2.1.0 jinja2==3.0.1 tables==3.6.1 blosc==1.10.2 From 14119d7366eabdd6ea4f56074590bacff0050143 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 05:22:30 +0000 Subject: [PATCH 0623/1386] Bump mkdocs-material from 7.1.6 to 7.1.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.6 to 7.1.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.6...7.1.7) --- 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 3f8020e8c..ff07905b7 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.6 +mkdocs-material==7.1.7 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 From 57cd8888e21ca457a8c3a408e9c3f838f228ea8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 17:34:25 +0000 Subject: [PATCH 0624/1386] Bump blosc from 1.10.2 to 1.10.4 Bumps [blosc](https://github.com/blosc/python-blosc) from 1.10.2 to 1.10.4. - [Release notes](https://github.com/blosc/python-blosc/releases) - [Changelog](https://github.com/Blosc/python-blosc/blob/master/RELEASE_NOTES.rst) - [Commits](https://github.com/blosc/python-blosc/compare/v1.10.2...v1.10.4) --- updated-dependencies: - dependency-name: blosc 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 6b7331e20..0e4d35464 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ tabulate==0.8.9 pycoingecko==2.1.0 jinja2==3.0.1 tables==3.6.1 -blosc==1.10.2 +blosc==1.10.4 # find first, C search in arrays py_find_1st==1.1.5 From dff8490daa14b44c4f7a4bfc57e7eadbf20b59e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Jun 2021 20:00:30 +0200 Subject: [PATCH 0625/1386] Fix docs rendering for pricefilter --- docs/includes/pairlists.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index ce0cc6e57..f19c5a181 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -122,8 +122,8 @@ The `max_price` setting removes pairs where the price is above the specified pri This option is disabled by default, and will only apply if set to > 0. The `max_value` setting removes pairs where the minimum value change is above a specified value. -This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20$) as the coin has risen sharply since the last limit adaption. -As a result of the above, you can only buy for 20$, or 40$ - but not for 25$. +This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20\$) as the coin has risen sharply since the last limit adaption. +As a result of the above, you can only buy for 20\$, or 40\$ - but not for 25\$. On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit. The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. From 97a12ddab703ac98278b69b0cfac2ed7f90c0a9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Jun 2021 20:19:48 +0200 Subject: [PATCH 0626/1386] Version pin mkdocs to avoid nasty surprises fix use_directory_urls defaulting to false --- docs/requirements-docs.txt | 1 + mkdocs.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ff07905b7..776d5592a 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,4 @@ +mkdocs==1.2 mkdocs-material==7.1.7 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 diff --git a/mkdocs.yml b/mkdocs.yml index 2520ca929..cc5747225 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,7 @@ site_name: Freqtrade +site_url: https://www.freqtrade.io/ repo_url: https://github.com/freqtrade/freqtrade +use_directory_urls: True nav: - Home: index.md - Quickstart with Docker: docker_quickstart.md From 4512ece17db9708bdb238da9c1f29ca0c66dd1c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Jun 2021 21:05:25 +0200 Subject: [PATCH 0627/1386] Update Discord link --- .github/ISSUE_TEMPLATE/config.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/developer.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7591c1fb0..5014de46a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,5 +2,5 @@ blank_issues_enabled: false contact_links: - name: Discord Server - url: https://discord.gg/MA9v74M + url: https://discord.gg/p7nuUNVfP7 about: Ask a question or get community support from our Discord server diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c29d6e632..040cf3e98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Few pointers for contributions: - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). -If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +If you are unsure, discuss the feature on our [discord server](https://discord.gg/p7nuUNVfP7), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index ab9597a77..d4b2df642 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ The project is currently setup in two main branches: For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel. -Please check out our [discord server](https://discord.gg/MA9v74M). +Please check out our [discord server](https://discord.gg/p7nuUNVfP7). You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). @@ -178,7 +178,7 @@ to understand the requirements before sending your pull-requests. Coding is not a necessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `stable`. diff --git a/docs/developer.md b/docs/developer.md index 4b8c64530..d0731a233 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/p7nuUNVfP7) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) where you can ask questions. ## Documentation diff --git a/docs/faq.md b/docs/faq.md index 7233a92fe..e5da550fd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -156,7 +156,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/MA9v74M). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/p7nuUNVfP7). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: diff --git a/docs/index.md b/docs/index.md index c2b6d5629..871172cfc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -76,7 +76,7 @@ Alternatively For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel. -Please check out our [discord server](https://discord.gg/MA9v74M). +Please check out our [discord server](https://discord.gg/p7nuUNVfP7). You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). From 35d6140068c2bbc3bf9ebe6e3b6bdf2bad1ddabf Mon Sep 17 00:00:00 2001 From: Bruno Gouvea Date: Mon, 7 Jun 2021 17:53:19 -0300 Subject: [PATCH 0628/1386] Displays the max drawdown in the hyper optimization results table. --- freqtrade/optimize/hyperopt_tools.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index ebe405d07..29014277e 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -233,18 +233,20 @@ class HyperoptTools(): 'results_metrics.winsdrawslosses', 'results_metrics.profit_mean', 'results_metrics.profit_total_abs', 'results_metrics.profit_total', 'results_metrics.holding_avg', - 'loss', 'is_initial_point', 'is_best']] + 'loss', 'is_initial_point', 'is_best', 'results_metrics.max_drawdown', + 'results_metrics.max_drawdown_abs']] else: # Legacy mode trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', 'results_metrics.winsdrawslosses', 'results_metrics.avg_profit', 'results_metrics.total_profit', 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best']] + 'loss', 'is_initial_point', 'is_best', 'results_metrics.max_drawdown', + 'results_metrics.max_drawdown_abs']] trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', - 'Total profit', 'Profit', 'Avg duration', 'Objective', - 'is_initial_point', 'is_best'] + 'Total profit', 'Profit', 'Avg duration', 'Max Drawdown', 'max_drawdown_abs', + 'Objective', 'is_initial_point', 'is_best'] trials['is_profit'] = False trials.loc[trials['is_initial_point'], 'Best'] = '* ' trials.loc[trials['is_best'], 'Best'] = 'Best' @@ -266,6 +268,16 @@ class HyperoptTools(): lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') ) + trials['Max Drawdown'] = trials.apply( + lambda x: '{} {}'.format( + round_coin_value(x['max_drawdown_abs'], stake_currency), + '({:,.2f}%)'.format(x['Max Drawdown'] * perc_multi).rjust(10, ' ') + ).rjust(25 + len(stake_currency)) + if x['Max Drawdown'] != 0.0 else '--'.rjust(25 + len(stake_currency)), + axis=1 + ) + trials = trials.drop(columns=['max_drawdown_abs']) + stake_currency = config['stake_currency'] trials['Profit'] = trials.apply( lambda x: '{} {}'.format( From 5c3a418e6564d92c83cf123f150a6a87bb2cd81f Mon Sep 17 00:00:00 2001 From: Bruno Gouvea Date: Mon, 7 Jun 2021 18:15:26 -0300 Subject: [PATCH 0629/1386] Adjusting drawdown column position. --- freqtrade/optimize/hyperopt_tools.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 29014277e..9d6248643 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -233,16 +233,15 @@ class HyperoptTools(): 'results_metrics.winsdrawslosses', 'results_metrics.profit_mean', 'results_metrics.profit_total_abs', 'results_metrics.profit_total', 'results_metrics.holding_avg', - 'loss', 'is_initial_point', 'is_best', 'results_metrics.max_drawdown', - 'results_metrics.max_drawdown_abs']] + 'results_metrics.max_drawdown', 'results_metrics.max_drawdown_abs', + 'loss', 'is_initial_point', 'is_best']] else: # Legacy mode trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', 'results_metrics.winsdrawslosses', 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best', 'results_metrics.max_drawdown', - 'results_metrics.max_drawdown_abs']] + 'results_metrics.profit', 'results_metrics.duration', 'results_metrics.max_drawdown', + 'results_metrics.max_drawdown_abs', 'loss', 'is_initial_point', 'is_best']] trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', 'Total profit', 'Profit', 'Avg duration', 'Max Drawdown', 'max_drawdown_abs', @@ -268,6 +267,8 @@ class HyperoptTools(): lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') ) + stake_currency = config['stake_currency'] + trials['Max Drawdown'] = trials.apply( lambda x: '{} {}'.format( round_coin_value(x['max_drawdown_abs'], stake_currency), @@ -278,7 +279,7 @@ class HyperoptTools(): ) trials = trials.drop(columns=['max_drawdown_abs']) - stake_currency = config['stake_currency'] + trials['Profit'] = trials.apply( lambda x: '{} {}'.format( round_coin_value(x['Total profit'], stake_currency), From c513c9685d4ff57f3a586430355fe221dc96c7af Mon Sep 17 00:00:00 2001 From: Bruno Gouvea Date: Mon, 7 Jun 2021 18:20:04 -0300 Subject: [PATCH 0630/1386] Remove blank line (PEP8) --- freqtrade/optimize/hyperopt_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 9d6248643..59332ef69 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -279,7 +279,6 @@ class HyperoptTools(): ) trials = trials.drop(columns=['max_drawdown_abs']) - trials['Profit'] = trials.apply( lambda x: '{} {}'.format( round_coin_value(x['Total profit'], stake_currency), From 4595db39aa95b2da1d9d3c8d297072a6dfdf2fa2 Mon Sep 17 00:00:00 2001 From: Bruno Gouvea Date: Tue, 8 Jun 2021 02:18:00 -0300 Subject: [PATCH 0631/1386] Displaying max. drawdown only when it is not legacy mode. --- freqtrade/optimize/hyperopt_tools.py | 39 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 59332ef69..4243c182d 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -221,6 +221,7 @@ class HyperoptTools(): if 'results_metrics.winsdrawslosses' not in trials.columns: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' + legacy_mode = True if 'results_metrics.total_trades' in trials: @@ -235,17 +236,22 @@ class HyperoptTools(): 'results_metrics.profit_total', 'results_metrics.holding_avg', 'results_metrics.max_drawdown', 'results_metrics.max_drawdown_abs', 'loss', 'is_initial_point', 'is_best']] + + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', + 'Total profit', 'Profit', 'Avg duration', 'Max Drawdown', + 'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_best'] else: # Legacy mode trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.winsdrawslosses', - 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', 'results_metrics.max_drawdown', - 'results_metrics.max_drawdown_abs', 'loss', 'is_initial_point', 'is_best']] + 'results_metrics.winsdrawslosses', 'results_metrics.avg_profit', + 'results_metrics.total_profit', 'results_metrics.profit', + 'results_metrics.duration', 'loss', 'is_initial_point', + 'is_best']] + + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', + 'Total profit', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] - trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', - 'Total profit', 'Profit', 'Avg duration', 'Max Drawdown', 'max_drawdown_abs', - 'Objective', 'is_initial_point', 'is_best'] trials['is_profit'] = False trials.loc[trials['is_initial_point'], 'Best'] = '* ' trials.loc[trials['is_best'], 'Best'] = 'Best' @@ -269,15 +275,16 @@ class HyperoptTools(): stake_currency = config['stake_currency'] - trials['Max Drawdown'] = trials.apply( - lambda x: '{} {}'.format( - round_coin_value(x['max_drawdown_abs'], stake_currency), - '({:,.2f}%)'.format(x['Max Drawdown'] * perc_multi).rjust(10, ' ') - ).rjust(25 + len(stake_currency)) - if x['Max Drawdown'] != 0.0 else '--'.rjust(25 + len(stake_currency)), - axis=1 - ) - trials = trials.drop(columns=['max_drawdown_abs']) + if not legacy_mode: + trials['Max Drawdown'] = trials.apply( + lambda x: '{} {}'.format( + round_coin_value(x['max_drawdown_abs'], stake_currency), + '({:,.2f}%)'.format(x['Max Drawdown'] * perc_multi).rjust(10, ' ') + ).rjust(25 + len(stake_currency)) + if x['Max Drawdown'] != 0.0 else '--'.rjust(25 + len(stake_currency)), + axis=1 + ) + trials = trials.drop(columns=['max_drawdown_abs']) trials['Profit'] = trials.apply( lambda x: '{} {}'.format( From 816bb531b32e28dee9362395167bdbb05677a583 Mon Sep 17 00:00:00 2001 From: Bruno Gouvea Date: Tue, 8 Jun 2021 02:42:55 -0300 Subject: [PATCH 0632/1386] Creating fake column for legacy mode on max drawdown --- freqtrade/optimize/hyperopt_tools.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 4243c182d..a739ab9b9 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -222,6 +222,11 @@ class HyperoptTools(): # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' + if 'results_metrics.max_drawdown_abs' not in trials.columns: + # Ensure compatibility with older versions of hyperopt results + trials['results_metrics.max_drawdown_abs'] = None + trials['results_metrics.max_drawdown'] = None + legacy_mode = True if 'results_metrics.total_trades' in trials: @@ -237,20 +242,18 @@ class HyperoptTools(): 'results_metrics.max_drawdown', 'results_metrics.max_drawdown_abs', 'loss', 'is_initial_point', 'is_best']] - trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', - 'Total profit', 'Profit', 'Avg duration', 'Max Drawdown', - 'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_best'] else: # Legacy mode trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', 'results_metrics.winsdrawslosses', 'results_metrics.avg_profit', 'results_metrics.total_profit', 'results_metrics.profit', - 'results_metrics.duration', 'loss', 'is_initial_point', + 'results_metrics.duration', 'results_metrics.max_drawdown', + 'results_metrics.max_drawdown_abs', 'loss', 'is_initial_point', 'is_best']] - trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', - 'Total profit', 'Profit', 'Avg duration', 'Objective', - 'is_initial_point', 'is_best'] + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', + 'Total profit', 'Profit', 'Avg duration', 'Max Drawdown', + 'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_best'] trials['is_profit'] = False trials.loc[trials['is_initial_point'], 'Best'] = '* ' @@ -284,7 +287,10 @@ class HyperoptTools(): if x['Max Drawdown'] != 0.0 else '--'.rjust(25 + len(stake_currency)), axis=1 ) - trials = trials.drop(columns=['max_drawdown_abs']) + else: + trials = trials.drop(columns=['Max Drawdown']) + + trials = trials.drop(columns=['max_drawdown_abs']) trials['Profit'] = trials.apply( lambda x: '{} {}'.format( From 3cce668353322e3d134b94442d218141e22286b5 Mon Sep 17 00:00:00 2001 From: Bruno Gouvea Date: Tue, 8 Jun 2021 02:57:44 -0300 Subject: [PATCH 0633/1386] Creating a control variable to determine the existence of max drawdown in the final result. --- freqtrade/optimize/hyperopt_tools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index a739ab9b9..eaea4aa4a 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -222,10 +222,12 @@ class HyperoptTools(): # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' + has_drawdown = True if 'results_metrics.max_drawdown_abs' not in trials.columns: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.max_drawdown_abs'] = None trials['results_metrics.max_drawdown'] = None + has_drawdown = False legacy_mode = True @@ -278,7 +280,7 @@ class HyperoptTools(): stake_currency = config['stake_currency'] - if not legacy_mode: + if has_drawdown: trials['Max Drawdown'] = trials.apply( lambda x: '{} {}'.format( round_coin_value(x['max_drawdown_abs'], stake_currency), From 3310a4502977a730b34fac79c2d657d631edc2f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jun 2021 20:10:43 +0200 Subject: [PATCH 0634/1386] Change wording if limited lookback is used --- freqtrade/rpc/telegram.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6c7fa0493..0a5125020 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -427,6 +427,7 @@ class Telegram(RPCHandler): fiat_disp_cur = self._config.get('fiat_display_currency', '') start_date = datetime.fromtimestamp(0) + timescale = None try: if context.args: timescale = int(context.args[0]) @@ -466,16 +467,18 @@ class Telegram(RPCHandler): else: markdown_msg = "`No closed trade` \n" - markdown_msg += (f"*ROI:* All trades\n" - f"∙ `{round_coin_value(profit_all_coin, stake_cur)} " - f"({profit_all_percent_mean:.2f}%) " - f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" - f"*Total Trade Count:* `{trade_count}`\n" - f"*First Trade opened:* `{first_trade_date}`\n" - f"*Latest Trade opened:* `{latest_trade_date}\n`" - f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" - ) + markdown_msg += ( + f"*ROI:* All trades\n" + f"∙ `{round_coin_value(profit_all_coin, stake_cur)} " + f"({profit_all_percent_mean:.2f}%) " + f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" + f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" + f"*Total Trade Count:* `{trade_count}`\n" + f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " + f"`{first_trade_date}`\n" + f"*Latest Trade opened:* `{latest_trade_date}\n`" + f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" + ) if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") From b9cf950bbffcfab687856b0ccde71d8cc9225654 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jun 2021 20:35:25 +0200 Subject: [PATCH 0635/1386] Add test for bad argument on /profit --- tests/rpc/test_rpc_telegram.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7282c79bb..ae169e6e7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -451,8 +451,10 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) - - telegram._profit(update=update, context=MagicMock()) + context = MagicMock() + # Test with invalid 2nd argument (should silently pass) + context.args = ["aaa"] + telegram._profit(update=update, context=context) 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] From d16a61948958ed1c2a05c777c3084e06b10b43ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jun 2021 21:04:34 +0200 Subject: [PATCH 0636/1386] Move SellType Enum to it's own module --- freqtrade/edge/edge_positioning.py | 3 ++- freqtrade/enums/__init__.py | 2 ++ freqtrade/enums/selltype.py | 21 +++++++++++++++++++ freqtrade/freqtradebot.py | 5 +++-- freqtrade/optimize/backtesting.py | 3 ++- .../plugins/protections/stoploss_guard.py | 2 +- freqtrade/rpc/rpc.py | 3 ++- freqtrade/strategy/interface.py | 20 +----------------- 8 files changed, 34 insertions(+), 25 deletions(-) create mode 100644 freqtrade/enums/__init__.py create mode 100644 freqtrade/enums/selltype.py diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 4bc0d660b..418a12076 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -13,11 +13,12 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data +from freqtrade.enums import SellType from freqtrade.exceptions import OperationalException from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.state import RunMode -from freqtrade.strategy.interface import IStrategy, SellType +from freqtrade.strategy.interface import IStrategy logger = logging.getLogger(__name__) diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py new file mode 100644 index 000000000..f069c6152 --- /dev/null +++ b/freqtrade/enums/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from freqtrade.enums.selltype import SellType diff --git a/freqtrade/enums/selltype.py b/freqtrade/enums/selltype.py new file mode 100644 index 000000000..e9e3dcb5a --- /dev/null +++ b/freqtrade/enums/selltype.py @@ -0,0 +1,21 @@ + +from enum import Enum + + +class SellType(Enum): + """ + Enum to distinguish between sell reasons + """ + ROI = "roi" + STOP_LOSS = "stop_loss" + STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" + TRAILING_STOP_LOSS = "trailing_stop_loss" + SELL_SIGNAL = "sell_signal" + FORCE_SELL = "force_sell" + EMERGENCY_SELL = "emergency_sell" + CUSTOM_SELL = "custom_sell" + NONE = "" + + def __str__(self): + # explicitly convert to String to help with exporting data. + return self.value diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8628931b6..e854038d7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,6 +16,7 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge +from freqtrade.enums import SellType from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -27,7 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State -from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType +from freqtrade.strategy.interface import IStrategy, SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -337,7 +338,7 @@ class FreqtradeBot(LoggingMixin): # Assume this as the open order trade.open_order_id = order.order_id if fo: - logger.info(f"Found {order} for trade {trade}.jj") + logger.info(f"Found {order} for trade {trade}.") self.update_trade_state(trade, order.order_id, fo, stoploss_order=order.ft_order_side == 'stoploss') diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cbc0995aa..922f89c22 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -17,6 +17,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe from freqtrade.data.converter import trim_dataframes from freqtrade.data.dataprovider import DataProvider +from freqtrade.enums import SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin @@ -26,7 +27,7 @@ from freqtrade.persistence import LocalTrade, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType +from freqtrade.strategy.interface import IStrategy, SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 635c0be04..45d393411 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -3,9 +3,9 @@ import logging from datetime import datetime, timedelta from typing import Any, Dict +from freqtrade.enums import SellType from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn -from freqtrade.strategy.interface import SellType logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index be7d7e5ba..6245243aa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -15,6 +15,7 @@ from pandas import DataFrame from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data +from freqtrade.enums import SellType from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -24,7 +25,7 @@ from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.strategy.interface import SellCheckTuple, SellType +from freqtrade.strategy.interface import SellCheckTuple logger = logging.getLogger(__name__) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e2cde52eb..af1a57691 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade +from freqtrade.enums import SellType from freqtrade.strategy.hyper import HyperStrategyMixin from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -35,25 +36,6 @@ class SignalType(Enum): SELL = "sell" -class SellType(Enum): - """ - Enum to distinguish between sell reasons - """ - ROI = "roi" - STOP_LOSS = "stop_loss" - STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" - TRAILING_STOP_LOSS = "trailing_stop_loss" - SELL_SIGNAL = "sell_signal" - FORCE_SELL = "force_sell" - EMERGENCY_SELL = "emergency_sell" - CUSTOM_SELL = "custom_sell" - NONE = "" - - def __str__(self): - # explicitly convert to String to help with exporting data. - return self.value - - class SellCheckTuple(object): """ NamedTuple for Sell type + reason From 89b9915c12b9f7682e1961a7ba9487105a0ef4f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jun 2021 21:06:47 +0200 Subject: [PATCH 0637/1386] Update imports for SellType in tests --- freqtrade/strategy/interface.py | 2 +- tests/edge/test_edge.py | 2 +- tests/optimize/__init__.py | 2 +- tests/optimize/conftest.py | 2 +- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_backtesting.py | 2 +- tests/optimize/test_hyperopt.py | 2 +- tests/optimize/test_optimize_reports.py | 2 +- tests/plugins/test_protections.py | 2 +- tests/rpc/test_rpc_telegram.py | 2 +- tests/rpc/test_rpc_webhook.py | 2 +- tests/strategy/test_interface.py | 3 ++- tests/test_freqtradebot.py | 3 ++- tests/test_integration.py | 3 ++- 14 files changed, 17 insertions(+), 14 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index af1a57691..b60ec56f8 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -14,11 +14,11 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider +from freqtrade.enums import SellType from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade -from freqtrade.enums import SellType from freqtrade.strategy.hyper import HyperStrategyMixin from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index c4620e1c7..0655b3a0f 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -12,8 +12,8 @@ from pandas import DataFrame, to_datetime from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo +from freqtrade.enums import SellType from freqtrade.exceptions import OperationalException -from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset) diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 306850ff6..7cca2b1a8 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -3,8 +3,8 @@ from typing import Dict, List, NamedTuple, Optional import arrow from pandas import DataFrame +from freqtrade.enums import SellType from freqtrade.exchange import timeframe_to_minutes -from freqtrade.strategy.interface import SellType tests_start_time = arrow.get(2018, 10, 3) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 11b4674f3..59ba3015d 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -5,9 +5,9 @@ from pathlib import Path import pandas as pd import pytest +from freqtrade.enums import SellType from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index c35a083ec..e5b969383 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -4,8 +4,8 @@ import logging import pytest from freqtrade.data.history import get_timerange +from freqtrade.enums import SellType from freqtrade.optimize.backtesting import Backtesting -from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset, tests_timeframe) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 632d458ce..ed8cf4bcf 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -16,12 +16,12 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange +from freqtrade.enums import SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 07d208210..a1859cec3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -13,6 +13,7 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data +from freqtrade.enums import SellType from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto @@ -22,7 +23,6 @@ from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.strategy.hyper import IntParameter -from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index f5c9a5a24..3f31efb95 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -12,6 +12,7 @@ from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data import history from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data from freqtrade.edge import PairInfo +from freqtrade.enums import SellType from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, @@ -20,7 +21,6 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver -from freqtrade.strategy.interface import SellType from tests.data.test_history import _backup_file, _clean_test_file diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index a39301145..10ab64690 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -4,9 +4,9 @@ from datetime import datetime, timedelta import pytest from freqtrade import constants +from freqtrade.enums import SellType from freqtrade.persistence import PairLocks, Trade from freqtrade.plugins.protectionmanager import ProtectionManager -from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has_re diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ae169e6e7..ed952efad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -17,6 +17,7 @@ from telegram.error import NetworkError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo +from freqtrade.enums import SellType from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging @@ -24,7 +25,6 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import RunMode, State -from freqtrade.strategy.interface import SellType from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, patch_get_signal, patch_whitelist) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 0560f8d53..43d5dbdc5 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -5,9 +5,9 @@ from unittest.mock import MagicMock import pytest from requests import RequestException +from freqtrade.enums import SellType from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc.webhook import Webhook -from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index df487986f..64081fa37 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -10,12 +10,13 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data +from freqtrade.enums import SellType from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) -from freqtrade.strategy.interface import SellCheckTuple, SellType +from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 66866a8fc..6d74c3844 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -11,6 +11,7 @@ import arrow import pytest from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT +from freqtrade.enums import SellType from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) @@ -19,7 +20,7 @@ from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State -from freqtrade.strategy.interface import SellCheckTuple, SellType +from freqtrade.strategy.interface import SellCheckTuple from freqtrade.worker import Worker from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, diff --git a/tests/test_integration.py b/tests/test_integration.py index 33b3e1140..b12959a03 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -2,9 +2,10 @@ from unittest.mock import MagicMock import pytest +from freqtrade.enums import SellType from freqtrade.persistence import Trade from freqtrade.rpc.rpc import RPC -from freqtrade.strategy.interface import SellCheckTuple, SellType +from freqtrade.strategy.interface import SellCheckTuple from tests.conftest import get_patched_freqtradebot, patch_get_signal From 3c149b9b5921d4987da97a9934515ae008cf844b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jun 2021 21:09:39 +0200 Subject: [PATCH 0638/1386] Move signalType to enums --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/signaltype.py | 9 +++++++++ freqtrade/strategy/interface.py | 10 +--------- 3 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 freqtrade/enums/signaltype.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index f069c6152..b4037ec12 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa: F401 from freqtrade.enums.selltype import SellType +from freqtrade.enums.signaltype import SignalType diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py new file mode 100644 index 000000000..d636f378a --- /dev/null +++ b/freqtrade/enums/signaltype.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class SignalType(Enum): + """ + Enum to distinguish between buy and sell signals + """ + BUY = "buy" + SELL = "sell" diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b60ec56f8..834495a19 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -14,7 +14,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import SellType +from freqtrade.enums import SellType, SignalType from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date @@ -28,14 +28,6 @@ logger = logging.getLogger(__name__) CUSTOM_SELL_MAX_LENGTH = 64 -class SignalType(Enum): - """ - Enum to distinguish between buy and sell signals - """ - BUY = "buy" - SELL = "sell" - - class SellCheckTuple(object): """ NamedTuple for Sell type + reason From 9c34304cb979de092885e637e83a675784b2bdd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jun 2021 21:20:35 +0200 Subject: [PATCH 0639/1386] Move state enums to enums package --- freqtrade/commands/data_commands.py | 2 +- freqtrade/commands/deploy_commands.py | 2 +- freqtrade/commands/hyperopt_commands.py | 2 +- freqtrade/commands/list_commands.py | 2 +- freqtrade/commands/optimize_commands.py | 2 +- freqtrade/commands/pairlist_commands.py | 2 +- freqtrade/commands/plot_commands.py | 2 +- freqtrade/configuration/check_exchange.py | 2 +- freqtrade/configuration/config_setup.py | 2 +- freqtrade/configuration/config_validation.py | 2 +- freqtrade/configuration/configuration.py | 2 +- freqtrade/data/dataprovider.py | 2 +- freqtrade/edge/edge_positioning.py | 3 +-- freqtrade/enums/__init__.py | 2 ++ freqtrade/{state.py => enums/runmode.py} | 12 ------------ freqtrade/enums/state.py | 18 ++++++++++++++++++ freqtrade/freqtradebot.py | 3 +-- freqtrade/rpc/rpc.py | 3 +-- freqtrade/strategy/hyper.py | 2 +- freqtrade/wallets.py | 2 +- freqtrade/worker.py | 2 +- tests/commands/test_commands.py | 2 +- tests/conftest.py | 2 +- tests/data/test_dataprovider.py | 2 +- tests/optimize/conftest.py | 3 +-- tests/optimize/test_backtesting.py | 3 +-- tests/optimize/test_edge_cli.py | 2 +- tests/optimize/test_hyperopt.py | 3 +-- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 +- tests/rpc/test_rpc_telegram.py | 3 +-- tests/test_configuration.py | 2 +- tests/test_freqtradebot.py | 3 +-- tests/test_main.py | 2 +- tests/test_worker.py | 2 +- 35 files changed, 52 insertions(+), 52 deletions(-) rename freqtrade/{state.py => enums/runmode.py} (78%) create mode 100644 freqtrade/enums/state.py diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 58191ddb4..3877e0801 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -8,11 +8,11 @@ from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import ExchangeResolver -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 5ba3db9f9..cc0d653b9 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -8,9 +8,9 @@ import requests from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.misc import render_template, render_template_with_fallback -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index d8b00f369..dba4179ff 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -6,9 +6,9 @@ from colorama import init as colorama_init from freqtrade.configuration import setup_utils_configuration from freqtrade.data.btanalysis import get_latest_hyperopt_file +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.optimize.optimize_reports import show_backtest_result -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 167847a0d..f59adb5d8 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -12,11 +12,11 @@ from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import market_is_active, validate_exchanges from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 6323bc2b1..a84b3b3bd 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -3,9 +3,9 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.misc import round_coin_value -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index a2b10a42f..9f7a5958e 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -4,8 +4,8 @@ from typing import Any, Dict import rapidjson from freqtrade.configuration import setup_utils_configuration +from freqtrade.enums import RunMode from freqtrade.resolvers import ExchangeResolver -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/plot_commands.py b/freqtrade/commands/plot_commands.py index 5e547acb0..73a429a28 100644 --- a/freqtrade/commands/plot_commands.py +++ b/freqtrade/commands/plot_commands.py @@ -1,8 +1,8 @@ from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from freqtrade.state import RunMode def validate_plot_args(args: Dict[str, Any]) -> None: diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index 832caf153..f282447d4 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -1,10 +1,10 @@ import logging from typing import Any, Dict +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt, is_exchange_officially_supported, validate_exchange) -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/config_setup.py b/freqtrade/configuration/config_setup.py index 3b0f778f4..cd8464ead 100644 --- a/freqtrade/configuration/config_setup.py +++ b/freqtrade/configuration/config_setup.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict -from freqtrade.state import RunMode +from freqtrade.enums import RunMode from .check_exchange import remove_credentials from .config_validation import validate_config_consistency diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 31e38d572..3004d6bf7 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -6,8 +6,8 @@ from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import constants +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index abb9f994b..bfeb2da5c 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -12,10 +12,10 @@ from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir from freqtrade.configuration.load_config import load_config_file, load_file +from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode from freqtrade.exceptions import OperationalException from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts -from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 1a86eece5..391ed4587 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -12,9 +12,9 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.data.history import load_pair_history +from freqtrade.enums import RunMode from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 418a12076..9466a1649 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -13,11 +13,10 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data -from freqtrade.enums import SellType +from freqtrade.enums import RunMode, SellType from freqtrade.exceptions import OperationalException from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist -from freqtrade.state import RunMode from freqtrade.strategy.interface import IStrategy diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index b4037ec12..f0092ffdf 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,3 +1,5 @@ # flake8: noqa: F401 +from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType from freqtrade.enums.signaltype import SignalType +from freqtrade.enums.state import State diff --git a/freqtrade/state.py b/freqtrade/enums/runmode.py similarity index 78% rename from freqtrade/state.py rename to freqtrade/enums/runmode.py index 8ddff71d9..06ebf91a2 100644 --- a/freqtrade/state.py +++ b/freqtrade/enums/runmode.py @@ -6,18 +6,6 @@ Bot state constant from enum import Enum -class State(Enum): - """ - Bot application states - """ - RUNNING = 1 - STOPPED = 2 - RELOAD_CONFIG = 3 - - def __str__(self): - return f"{self.name.lower()}" - - class RunMode(Enum): """ Bot running mode (backtest, hyperopt, ...) diff --git a/freqtrade/enums/state.py b/freqtrade/enums/state.py new file mode 100644 index 000000000..d6c611ea0 --- /dev/null +++ b/freqtrade/enums/state.py @@ -0,0 +1,18 @@ +# pragma pylint: disable=too-few-public-methods + +""" +Bot state constant +""" +from enum import Enum + + +class State(Enum): + """ + Bot application states + """ + RUNNING = 1 + STOPPED = 2 + RELOAD_CONFIG = 3 + + def __str__(self): + return f"{self.name.lower()}" diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e854038d7..1efe277cc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import SellType +from freqtrade.enums import SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -27,7 +27,6 @@ from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType -from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 6245243aa..0b7a4152a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -15,7 +15,7 @@ from pandas import DataFrame from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data -from freqtrade.enums import SellType +from freqtrade.enums import SellType, State from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -24,7 +24,6 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter -from freqtrade.state import State from freqtrade.strategy.interface import SellCheckTuple diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index a320f2bb8..e487ffeff 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -14,8 +14,8 @@ with suppress(ImportError): from skopt.space import Integer, Real, Categorical from freqtrade.optimize.space import SKDecimal +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index d5f979c24..1b2ec4550 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -8,10 +8,10 @@ from typing import Any, Dict, NamedTuple import arrow from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.enums import RunMode from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import LocalTrade, Trade -from freqtrade.state import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index ec9331eef..4fa9166bf 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -11,9 +11,9 @@ import sdnotify from freqtrade import __version__, constants from freqtrade.configuration import Configuration +from freqtrade.enums import State from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.state import State logger = logging.getLogger(__name__) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index b0808ae06..47f298ad7 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -17,8 +17,8 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) from freqtrade.configuration import setup_utils_configuration +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from freqtrade.state import RunMode from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) from tests.conftest_trades import MOCK_TRADE_COUNT diff --git a/tests/conftest.py b/tests/conftest.py index 43a98647f..dd38ca610 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,11 +17,11 @@ from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo +from freqtrade.enums import RunMode from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver -from freqtrade.state import RunMode from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, mock_trade_5, mock_trade_6) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index b87258c64..e43309743 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -5,9 +5,9 @@ import pytest from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider +from freqtrade.enums import RunMode from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.plugins.pairlistmanager import PairListManager -from freqtrade.state import RunMode from tests.conftest import get_patched_exchange diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 59ba3015d..a7fd238d1 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -5,9 +5,8 @@ from pathlib import Path import pandas as pd import pytest -from freqtrade.enums import SellType +from freqtrade.enums import RunMode, SellType from freqtrade.optimize.hyperopt import Hyperopt -from freqtrade.state import RunMode from tests.conftest import patch_exchange diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index ed8cf4bcf..7387c8865 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -16,12 +16,11 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.enums import SellType +from freqtrade.enums import RunMode, SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver -from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 188b4aa5f..6818a573b 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge +from freqtrade.enums import RunMode from freqtrade.optimize.edge_cli import EdgeCli -from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a1859cec3..e5c05e4c0 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -13,7 +13,7 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data -from freqtrade.enums import SellType +from freqtrade.enums import RunMode, SellType from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto @@ -21,7 +21,6 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver -from freqtrade.state import RunMode from freqtrade.strategy.hyper import IntParameter from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 7556dde6d..8a41099f4 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,12 +8,12 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo +from freqtrade.enums import State from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter -from freqtrade.state import State from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index def2e43c6..8d48e2286 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -15,6 +15,7 @@ from numpy import isnan from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ +from freqtrade.enums import RunMode, State from freqtrade.exceptions import ExchangeError from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade @@ -22,7 +23,6 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer -from freqtrade.state import RunMode, State from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ed952efad..7ff9cda9e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -17,14 +17,13 @@ from telegram.error import NetworkError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo -from freqtrade.enums import SellType +from freqtrade.enums import RunMode, SellType, State from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only -from freqtrade.state import RunMode, State from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, patch_get_signal, patch_whitelist) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index b08d0775c..aa121edfa 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -20,9 +20,9 @@ from freqtrade.configuration.deprecated_settings import (check_conflicting_setti process_temporary_deprecated_settings) from freqtrade.configuration.load_config import load_config_file, load_file, log_config_error_range from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre -from freqtrade.state import RunMode from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6d74c3844..1fecb3f6a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -11,7 +11,7 @@ import arrow import pytest from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT -from freqtrade.enums import SellType +from freqtrade.enums import RunMode, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) @@ -19,7 +19,6 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType -from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple from freqtrade.worker import Worker from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, diff --git a/tests/test_main.py b/tests/test_main.py index d52dcaf79..3546a3bab 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,10 +7,10 @@ from unittest.mock import MagicMock, PropertyMock import pytest from freqtrade.commands import Arguments +from freqtrade.enums import State from freqtrade.exceptions import FreqtradeException, OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main -from freqtrade.state import State from freqtrade.worker import Worker from tests.conftest import (log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) diff --git a/tests/test_worker.py b/tests/test_worker.py index 839f7cdac..c3773d296 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -3,7 +3,7 @@ import time from unittest.mock import MagicMock, PropertyMock from freqtrade.data.dataprovider import DataProvider -from freqtrade.state import State +from freqtrade.enums import State from freqtrade.worker import Worker from tests.conftest import get_patched_worker, log_has, log_has_re From 756904f9854abfd43e71d7df7677f217ab1f07a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jun 2021 21:21:29 +0200 Subject: [PATCH 0640/1386] Set sell_reason to stoploss when closing the trade as stoploss closes #5101 --- freqtrade/persistence/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ee934f657..984b2de27 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -14,6 +14,7 @@ from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.enums import SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -430,6 +431,7 @@ class LocalTrade(): elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss + self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value if self.is_open: logger.info(f'{order_type.upper()} is hit for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) From 40f1ede77549ee7a57645030812507a326378fb8 Mon Sep 17 00:00:00 2001 From: Bruno Gouvea Date: Wed, 9 Jun 2021 12:03:24 -0300 Subject: [PATCH 0641/1386] Simplifying HO's result function --- freqtrade/optimize/hyperopt_tools.py | 40 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index eaea4aa4a..6024422bc 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -206,33 +206,20 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, - print_colorized: bool, remove_header: int) -> str: - """ - Log result table - """ - if not results: - return '' + def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str: - tabulate.PRESERVE_WHITESPACE = True - - trials = json_normalize(results, max_level=1) trials['Best'] = '' + if 'results_metrics.winsdrawslosses' not in trials.columns: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' - has_drawdown = True - if 'results_metrics.max_drawdown_abs' not in trials.columns: + if not has_drawdown: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.max_drawdown_abs'] = None trials['results_metrics.max_drawdown'] = None - has_drawdown = False - legacy_mode = True - - if 'results_metrics.total_trades' in trials: - legacy_mode = False + if not legacy_mode: # New mode, using backtest result for metrics trials['results_metrics.winsdrawslosses'] = trials.apply( lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " @@ -257,6 +244,25 @@ class HyperoptTools(): 'Total profit', 'Profit', 'Avg duration', 'Max Drawdown', 'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_best'] + return trials + + @staticmethod + def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, + print_colorized: bool, remove_header: int) -> str: + """ + Log result table + """ + if not results: + return '' + + tabulate.PRESERVE_WHITESPACE = True + trials = json_normalize(results, max_level=1) + + legacy_mode = 'results_metrics.total_trades' not in trials + has_drawdown = 'results_metrics.max_drawdown_abs' in trials.columns + + trials = HyperoptTools.prepare_trials_columns(trials, legacy_mode, has_drawdown) + trials['is_profit'] = False trials.loc[trials['is_initial_point'], 'Best'] = '* ' trials.loc[trials['is_best'], 'Best'] = 'Best' From d4dfdf04fc1ba98710192ee9a83086e6e7bbeaba Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Jun 2021 19:51:44 +0200 Subject: [PATCH 0642/1386] Move RPCMessageType to enums --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/rpcmessagetype.py | 19 +++++++++++++++++++ freqtrade/enums/runmode.py | 5 ----- freqtrade/freqtradebot.py | 4 ++-- freqtrade/rpc/__init__.py | 2 +- freqtrade/rpc/rpc.py | 19 ------------------- freqtrade/rpc/rpc_manager.py | 3 ++- freqtrade/rpc/telegram.py | 3 ++- freqtrade/rpc/webhook.py | 3 ++- freqtrade/strategy/interface.py | 1 - tests/rpc/test_rpc_manager.py | 3 ++- tests/rpc/test_rpc_telegram.py | 4 ++-- tests/rpc/test_rpc_webhook.py | 4 ++-- tests/test_freqtradebot.py | 3 +-- 14 files changed, 36 insertions(+), 38 deletions(-) create mode 100644 freqtrade/enums/rpcmessagetype.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index f0092ffdf..78163d86f 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa: F401 +from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType from freqtrade.enums.signaltype import SignalType diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py new file mode 100644 index 000000000..9c59f6108 --- /dev/null +++ b/freqtrade/enums/rpcmessagetype.py @@ -0,0 +1,19 @@ +from enum import Enum + + +class RPCMessageType(Enum): + STATUS = 'status' + WARNING = 'warning' + STARTUP = 'startup' + BUY = 'buy' + BUY_FILL = 'buy_fill' + BUY_CANCEL = 'buy_cancel' + SELL = 'sell' + SELL_FILL = 'sell_fill' + SELL_CANCEL = 'sell_cancel' + + def __repr__(self): + return self.value + + def __str__(self): + return self.value diff --git a/freqtrade/enums/runmode.py b/freqtrade/enums/runmode.py index 06ebf91a2..7826d1d0c 100644 --- a/freqtrade/enums/runmode.py +++ b/freqtrade/enums/runmode.py @@ -1,8 +1,3 @@ -# pragma pylint: disable=too-few-public-methods - -""" -Bot state constant -""" from enum import Enum diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1efe277cc..7aca0fcb2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import SellType, State +from freqtrade.enums import RPCMessageType, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -26,7 +26,7 @@ from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.rpc import RPCManager, RPCMessageType +from freqtrade.rpc import RPCManager from freqtrade.strategy.interface import IStrategy, SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index 0a0130ca7..957565e2c 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -1,3 +1,3 @@ # flake8: noqa: F401 -from .rpc import RPC, RPCException, RPCHandler, RPCMessageType +from .rpc import RPC, RPCException, RPCHandler from .rpc_manager import RPCManager diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0b7a4152a..2a7721af0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -4,7 +4,6 @@ This module contains class to define a RPC communications import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone -from enum import Enum from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union @@ -30,24 +29,6 @@ from freqtrade.strategy.interface import SellCheckTuple logger = logging.getLogger(__name__) -class RPCMessageType(Enum): - STATUS = 'status' - WARNING = 'warning' - STARTUP = 'startup' - BUY = 'buy' - BUY_FILL = 'buy_fill' - BUY_CANCEL = 'buy_cancel' - SELL = 'sell' - SELL_FILL = 'sell_fill' - SELL_CANCEL = 'sell_cancel' - - def __repr__(self): - return self.value - - def __str__(self): - return self.value - - class RPCException(Exception): """ Should be raised with a rpc-formatted message in an _rpc_* method diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index f819b55b4..18ed68041 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -4,7 +4,8 @@ This module contains class to manage RPC communications (Telegram, Slack, ...) import logging from typing import Any, Dict, List -from freqtrade.rpc import RPC, RPCHandler, RPCMessageType +from freqtrade.enums import RPCMessageType +from freqtrade.rpc import RPC, RPCHandler logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8742d41e2..c3ddfd644 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -22,9 +22,10 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN +from freqtrade.enums import RPCMessageType from freqtrade.exceptions import OperationalException from freqtrade.misc import chunks, round_coin_value -from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType +from freqtrade.rpc import RPC, RPCException, RPCHandler logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 24e1348f1..0e4a4bf6f 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -6,7 +6,8 @@ from typing import Any, Dict from requests import RequestException, post -from freqtrade.rpc import RPC, RPCHandler, RPCMessageType +from freqtrade.enums import RPCMessageType +from freqtrade.rpc import RPC, RPCHandler logger = logging.getLogger(__name__) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 834495a19..8ea38f503 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,7 +6,6 @@ import logging import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone -from enum import Enum from typing import Dict, List, Optional, Tuple, Union import arrow diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 69a757fcf..918022386 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -3,7 +3,8 @@ import logging import time from unittest.mock import MagicMock -from freqtrade.rpc import RPCManager, RPCMessageType +from freqtrade.enums import RPCMessageType +from freqtrade.rpc import RPCManager from tests.conftest import get_patched_freqtradebot, log_has diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7ff9cda9e..bbda55c3e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -17,12 +17,12 @@ from telegram.error import NetworkError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo -from freqtrade.enums import RunMode, SellType, State +from freqtrade.enums import RPCMessageType, RunMode, SellType, State from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade -from freqtrade.rpc import RPC, RPCMessageType +from freqtrade.rpc import RPC from freqtrade.rpc.telegram import Telegram, authorized_only from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, patch_get_signal, patch_whitelist) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 43d5dbdc5..04e63a3be 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -5,8 +5,8 @@ from unittest.mock import MagicMock import pytest from requests import RequestException -from freqtrade.enums import SellType -from freqtrade.rpc import RPC, RPCMessageType +from freqtrade.enums import RPCMessageType, SellType +from freqtrade.rpc import RPC from freqtrade.rpc.webhook import Webhook from tests.conftest import get_patched_freqtradebot, log_has diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1fecb3f6a..66f87f7c9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -11,14 +11,13 @@ import arrow import pytest from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT -from freqtrade.enums import RunMode, SellType, State +from freqtrade.enums import RPCMessageType, RunMode, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock -from freqtrade.rpc import RPCMessageType from freqtrade.strategy.interface import SellCheckTuple from freqtrade.worker import Worker from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, From c2929260866771d4444d869f471d727a6b4169b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Jun 2021 06:21:10 +0200 Subject: [PATCH 0643/1386] Small style improvements (no empty line at start) --- freqtrade/enums/selltype.py | 1 - freqtrade/enums/state.py | 5 ----- 2 files changed, 6 deletions(-) diff --git a/freqtrade/enums/selltype.py b/freqtrade/enums/selltype.py index e9e3dcb5a..015c30186 100644 --- a/freqtrade/enums/selltype.py +++ b/freqtrade/enums/selltype.py @@ -1,4 +1,3 @@ - from enum import Enum diff --git a/freqtrade/enums/state.py b/freqtrade/enums/state.py index d6c611ea0..572e2299f 100644 --- a/freqtrade/enums/state.py +++ b/freqtrade/enums/state.py @@ -1,8 +1,3 @@ -# pragma pylint: disable=too-few-public-methods - -""" -Bot state constant -""" from enum import Enum From c215b24a1914ead4c28688da1115c2f0b1e4c65d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jun 2021 15:54:32 +0000 Subject: [PATCH 0644/1386] Bump fastapi from 0.65.1 to 0.65.2 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.65.1 to 0.65.2. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.65.1...0.65.2) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e4d35464..abfae55b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ python-rapidjson==1.0 sdnotify==0.3.2 # API Server -fastapi==0.65.1 +fastapi==0.65.2 uvicorn==0.14.0 pyjwt==2.1.0 aiofiles==0.7.0 From a05e38dbd340dab8500c05fcacb9b59d89baad82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Jun 2021 09:03:55 +0200 Subject: [PATCH 0645/1386] Require timeframe for plot-profit must be set in config or via --timeframe --- freqtrade/plot/plotting.py | 9 ++++++--- tests/test_plotting.py | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f52b0bc81..c1b1232c2 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -47,7 +47,7 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): data = load_data( datadir=config.get('datadir'), pairs=pairs, - timeframe=config.get('timeframe', '5m'), + timeframe=config['timeframe'], timerange=timerange, startup_candles=startup_candles, data_format=config.get('dataformat_ohlcv', 'json'), @@ -56,7 +56,7 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): if startup_candles and data: min_date, max_date = get_timerange(data) logger.info(f"Loading data from {min_date} to {max_date}") - timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')), + timerange.adjust_start_if_necessary(timeframe_to_seconds(config['timeframe']), startup_candles, min_date) no_trades = False @@ -583,6 +583,9 @@ def plot_profit(config: Dict[str, Any]) -> None: But should be somewhat proportional, and therefor useful in helping out to find a good algorithm. """ + if 'timeframe' not in config: + raise OperationalException('Timeframe must be set in either config or via --timeframe.') + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) plot_elements = init_plotscript(config, list(exchange.markets)) trades = plot_elements['trades'] @@ -599,7 +602,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], - trades, config.get('timeframe', '5m'), + trades, config['timeframe'], config.get('stake_currency', '')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / 'plot', diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 0b1054ced..20f159e3a 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -462,6 +462,10 @@ def test_plot_profit(default_conf, mocker, testdatadir): assert profit_mock.call_args_list[0][0][0] == default_conf['pairs'] assert store_mock.call_args_list[0][1]['auto_open'] is False + del default_conf['timeframe'] + with pytest.raises(OperationalException, match=r"Timeframe must be set.*--timeframe.*"): + plot_profit(default_conf) + @pytest.mark.parametrize("ind1,ind2,plot_conf,exp", [ # No indicators, use plot_conf From d35b2e3b8f545aee81742a8698d0db319210fa40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 11:06:34 +0200 Subject: [PATCH 0646/1386] Update ftx stoploss logic to properly detect correct trades closes #5045 --- freqtrade/exchange/ftx.py | 13 ++++++++++++- freqtrade/freqtradebot.py | 9 +++++++-- tests/exchange/test_ftx.py | 17 ++++++++++++++--- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 105389828..3184c2524 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -100,6 +100,17 @@ class Ftx(Exchange): order = [order for order in orders if order['id'] == order_id] if len(order) == 1: + if order[0].get('status') == 'closed': + # Trigger order was triggered ... + real_order_id = order[0].get('info', {}).get('orderId') + + order1 = self._api.fetch_order(real_order_id, pair) + # Fake type to stop - as this was really a stop order. + order1['id_stop'] = order1['id'] + order1['id'] = order_id + order1['type'] = 'stop' + order1['status_stop'] = 'triggered' + return order1 return order[0] else: raise InvalidOrderException(f"Could not get stoploss order for id {order_id}") @@ -134,5 +145,5 @@ class Ftx(Exchange): def get_order_id_conditional(self, order: Dict[str, Any]) -> str: if order['type'] == 'stop': - return safe_value_fallback2(order['info'], order, 'orderId', 'id') + return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7aca0fcb2..a2e7fcb5d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -816,8 +816,13 @@ class FreqtradeBot(LoggingMixin): logger.warning('Stoploss order was cancelled, but unable to recreate one.') # Finally we check if stoploss on exchange should be moved up because of trailing. - if stoploss_order and (self.config.get('trailing_stop', False) - or self.config.get('use_custom_stoploss', False)): + # Triggered Orders are now real orders - so don't replace stoploss anymore + if ( + 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 # value immediately diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 63d99acdf..3794bb79c 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -125,7 +125,7 @@ def test_stoploss_adjust_ftx(mocker, default_conf): assert not exchange.stoploss_adjust(1501, order) -def test_fetch_stoploss_order(default_conf, mocker): +def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 @@ -147,6 +147,17 @@ def test_fetch_stoploss_order(default_conf, mocker): with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] + api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': 'closed'}]) + api_mock.fetch_order = MagicMock(return_value=limit_sell_order) + + resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') + assert resp + assert api_mock.fetch_order.call_count == 1 + assert resp['id_stop'] == 'mocked_limit_sell' + assert resp['id'] == 'X' + assert resp['type'] == 'stop' + assert resp['status_stop'] == 'triggered' + with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') @@ -165,8 +176,8 @@ def test_get_order_id(mocker, default_conf): 'type': STOPLOSS_ORDERTYPE, 'price': 1500, 'id': '1111', + 'id_stop': '1234', 'info': { - 'orderId': '1234' } } assert exchange.get_order_id_conditional(order) == '1234' @@ -175,8 +186,8 @@ def test_get_order_id(mocker, default_conf): 'type': 'limit', 'price': 1500, 'id': '1111', + 'id_stop': '1234', 'info': { - 'orderId': '1234' } } assert exchange.get_order_id_conditional(order) == '1111' From c65b4e5d3baaf2046bad848a5e061b267f767458 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 11:17:44 +0200 Subject: [PATCH 0647/1386] Small fix to models --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 984b2de27..bbbc55c13 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -437,7 +437,7 @@ class LocalTrade(): self.close(safe_value_fallback(order, 'average', 'price')) else: raise ValueError(f'Unknown order type: {order_type}') - cleanup_db() + Trade.commit() def close(self, rate: float, *, show_msg: bool = True) -> None: """ From d54ee0eb040b975a0e71cb6ccc106babd2f55d12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 11:24:24 +0200 Subject: [PATCH 0648/1386] Refactor hyperopt_tools naming --- freqtrade/commands/hyperopt_commands.py | 6 +++--- freqtrade/optimize/hyperopt.py | 4 ++-- freqtrade/optimize/hyperopt_tools.py | 4 ++-- tests/optimize/test_hyperopt.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index dba4179ff..19337b407 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -67,7 +67,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if epochs and not no_details: sorted_epochs = sorted(epochs, key=itemgetter('loss')) results = sorted_epochs[0] - HyperoptTools.print_epoch_details(results, total_epochs, print_json, no_header) + HyperoptTools.show_epoch_details(results, total_epochs, print_json, no_header) if epochs and export_csv: HyperoptTools.export_csv_file( @@ -132,8 +132,8 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: show_backtest_result(metrics['strategy_name'], metrics, metrics['stake_currency']) - HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, - header_str="Epoch details") + HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header, + header_str="Epoch details") def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 49273d3de..c2b2b93cb 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -469,8 +469,8 @@ class Hyperopt: f"saved to '{self.results_file}'.") if self.current_best_epoch: - HyperoptTools.print_epoch_details(self.current_best_epoch, self.total_epochs, - self.print_json) + HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs, + self.print_json) else: # This is printed when Ctrl+C is pressed quickly, before first epochs have # a chance to be evaluated. diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 6024422bc..d31b956dd 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -73,8 +73,8 @@ class HyperoptTools(): return epochs @staticmethod - def print_epoch_details(results, total_epochs: int, print_json: bool, - no_header: bool = False, header_str: str = None) -> None: + def show_epoch_details(results, total_epochs: int, print_json: bool, + no_header: bool = False, header_str: str = None) -> None: """ Display details of the hyperopt result """ diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index e5c05e4c0..10e99395d 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1068,7 +1068,7 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No hyperopt.start() -def test_print_epoch_details(capsys): +def test_show_epoch_details(capsys): test_result = { 'params_details': { 'trailing': { @@ -1090,7 +1090,7 @@ def test_print_epoch_details(capsys): 'is_best': True } - HyperoptTools.print_epoch_details(test_result, 5, False, no_header=True) + HyperoptTools.show_epoch_details(test_result, 5, False, no_header=True) captured = capsys.readouterr() assert '# Trailing stop:' in captured.out # re.match(r"Pairs for .*", captured.out) From fb4dd6c2ac161fd5af1b901a0eab931d868a0673 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 11:34:44 +0200 Subject: [PATCH 0649/1386] Update test to cover this scenario --- tests/strategy/test_strategy_loading.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 965c3d37b..bd192ecb5 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -129,13 +129,16 @@ def test_strategy_override_minimal_roi(caplog, default_conf): default_conf.update({ 'strategy': 'DefaultStrategy', 'minimal_roi': { + "20": 0.1, "0": 0.5 } }) strategy = StrategyResolver.load_strategy(default_conf) assert strategy.minimal_roi[0] == 0.5 - assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog) + assert log_has( + "Override strategy 'minimal_roi' with value in config file: {'20': 0.1, '0': 0.5}.", + caplog) def test_strategy_override_stoploss(caplog, default_conf): From eaf0aac77e7e42767028250e8216bc8d5df706e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 11:45:23 +0200 Subject: [PATCH 0650/1386] Remove OrderedDict as we're no longer supporting python 3.6 --- freqtrade/commands/list_commands.py | 3 +-- freqtrade/optimize/hyperopt_tools.py | 20 ++++---------------- freqtrade/resolvers/strategy_resolver.py | 3 +-- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index f59adb5d8..cd26aa60e 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -1,7 +1,6 @@ import csv import logging import sys -from collections import OrderedDict from pathlib import Path from typing import Any, Dict, List @@ -154,7 +153,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: pairs_only=pairs_only, active_only=active_only) # Sort the pairs/markets by symbol - pairs = OrderedDict(sorted(pairs.items())) + pairs = dict(sorted(pairs.items())) except Exception as e: raise OperationalException(f"Cannot get markets. Reason: {e}") from e diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index d31b956dd..fb1a0f56a 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -1,7 +1,6 @@ import io import logging -from collections import OrderedDict from pathlib import Path from typing import Any, Dict, List @@ -111,16 +110,9 @@ class HyperoptTools(): if space in ['buy', 'sell']: result_dict.setdefault('params', {}).update(space_params) elif space == 'roi': - # TODO: get rid of OrderedDict when support for python 3.6 will be - # dropped (dicts keep the order as the language feature) - # Convert keys in min_roi dict to strings because # rapidjson cannot dump dicts with integer keys... - # OrderedDict is used to keep the numeric order of the items - # in the dict. - result_dict['minimal_roi'] = OrderedDict( - (str(k), v) for k, v in space_params.items() - ) + result_dict['minimal_roi'] = {str(k): v for k, v in space_params.items()} else: # 'stoploss', 'trailing' result_dict.update(space_params) @@ -132,13 +124,9 @@ class HyperoptTools(): if space == 'stoploss': result += f"stoploss = {space_params.get('stoploss')}" elif space == 'roi': - # TODO: get rid of OrderedDict when support for python 3.6 will be - # dropped (dicts keep the order as the language feature) - minimal_roi_result = rapidjson.dumps( - OrderedDict( - (str(k), v) for k, v in space_params.items() - ), - default=str, indent=4, number_mode=rapidjson.NM_NATIVE) + minimal_roi_result = rapidjson.dumps({ + str(k): v for k, v in space_params.items() + }, default=str, indent=4, number_mode=rapidjson.NM_NATIVE) result += f"minimal_roi = {minimal_roi_result}" elif space == 'trailing': diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 05fbac10d..6484f900b 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -6,7 +6,6 @@ This module load custom strategies import logging import tempfile from base64 import urlsafe_b64decode -from collections import OrderedDict from inspect import getfullargspec from pathlib import Path from typing import Any, Dict, Optional @@ -139,7 +138,7 @@ class StrategyResolver(IResolver): # Sort and apply type conversions if hasattr(strategy, 'minimal_roi'): - strategy.minimal_roi = OrderedDict(sorted( + strategy.minimal_roi = dict(sorted( {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(), key=lambda t: t[0])) if hasattr(strategy, 'stoploss'): From 03eff698291b16077163bfe153860276a2a48d28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 20:21:43 +0200 Subject: [PATCH 0651/1386] Simplify update message sending --- freqtrade/rpc/telegram.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ba9c6c0f6..921fdfe59 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -10,12 +10,12 @@ from datetime import date, datetime, timedelta from html import escape from itertools import chain from math import isnan -from typing import Any, Callable, Dict, List, Union, cast +from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode, - ReplyKeyboardMarkup, Update) +from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, + ParseMode, ReplyKeyboardMarkup, Update) from telegram.error import BadRequest, NetworkError, TelegramError from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -180,8 +180,8 @@ class Telegram(RPCHandler): for handle in handles: self._updater.dispatcher.add_handler(handle) - for handle in callbacks: - self._updater.dispatcher.add_handler(handle) + for callback in callbacks: + self._updater.dispatcher.add_handler(callback) self._updater.start_polling( bootstrap_retries=-1, @@ -422,9 +422,7 @@ class Telegram(RPCHandler): lines = message.split("\n") message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) if(messages_count == 1 and update.callback_query): - query = update.callback_query - self._update_msg(chat_id=query.message.chat_id, - message_id=query.message.message_id, + self._update_msg(query=update.callback_query, msg=f"