From 69d62ef38316df7aa33683badbcef6590253ac76 Mon Sep 17 00:00:00 2001 From: Eko Aprili Trisno Date: Thu, 4 Feb 2021 01:06:52 +0700 Subject: [PATCH 01/26] 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 02/26] 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 03/26] 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 04/26] 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 bd44deea0dbfbcf3a651d1533f04b019ec5291f5 Mon Sep 17 00:00:00 2001 From: Rikj000 Date: Mon, 24 May 2021 18:51:33 +0200 Subject: [PATCH 05/26] 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 03eff698291b16077163bfe153860276a2a48d28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Jun 2021 20:21:43 +0200 Subject: [PATCH 06/26] 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"
{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 07/26] 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 08/26] 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 6d5fc967147e0aae37423d1e0ee57de47a17a791 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 12 Jun 2021 10:16:30 +0300 Subject: [PATCH 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 a9f111dca0063790dadaebfad03c265c8e0842ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Jun 2021 19:50:49 +0200 Subject: [PATCH 14/26] 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 15/26] 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 16/26] :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 17/26] :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 e1010ff5923e4c68900c6e786bb3e6138ea1265c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Jun 2021 21:01:22 +0200 Subject: [PATCH 18/26] 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 19/26] 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 20/26] :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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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()