From ffc2de8d3310255ad6b8c94bbe3f4c833a374cd2 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Thu, 4 Nov 2021 20:47:01 +0100 Subject: [PATCH 01/76] WIP Add /weekly and /monthly to Telegram RPC Related to "Show average profit in overview" (#5527) Signed-off-by: Antoine Merino --- freqtrade/rpc/rpc.py | 85 ++++++++++++ freqtrade/rpc/telegram.py | 83 ++++++++++- requirements.txt | 2 + tests/rpc/test_rpc_telegram.py | 245 ++++++++++++++++++++++++++++++++- 4 files changed, 409 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d0858350c..cd2599fa4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -4,6 +4,7 @@ This module contains class to define a RPC communications import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone +from dateutil.relativedelta import relativedelta from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union @@ -289,6 +290,90 @@ class RPC: 'data': data } + def _rpc_weekly_profit( + self, timescale: int, + stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: + today = datetime.utcnow().date() + profit_weeks: Dict[date, Dict] = {} + + if not (isinstance(timescale, int) and timescale > 0): + raise RPCException('timescale must be an integer greater than 0') + + for week in range(0, timescale): + profitweek = today - timedelta(weeks=week) + trades = Trade.get_trades(trade_filter=[ + Trade.is_open.is_(False), + Trade.close_date >= profitweek, + Trade.close_date < (profitweek + timedelta(weeks=1)) + ]).order_by(Trade.close_date).all() + curweekprofit = sum( + trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) + profit_weeks[profitweek] = { + 'amount': curweekprofit, + 'trades': len(trades) + } + + data = [ + { + 'date': key, + 'abs_profit': value["amount"], + 'fiat_value': self._fiat_converter.convert_amount( + value['amount'], + stake_currency, + fiat_display_currency + ) if self._fiat_converter else 0, + 'trade_count': value["trades"], + } + for key, value in profit_weeks.items() + ] + return { + 'stake_currency': stake_currency, + 'fiat_display_currency': fiat_display_currency, + 'data': data + } + + def _rpc_monthly_profit( + self, timescale: int, + stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: + today = datetime.utcnow().date() + profit_months: Dict[date, Dict] = {} + + if not (isinstance(timescale, int) and timescale > 0): + raise RPCException('timescale must be an integer greater than 0') + + for month in range(0, timescale): + profitmonth = today - relativedelta(months=month) + trades = Trade.get_trades(trade_filter=[ + Trade.is_open.is_(False), + Trade.close_date >= profitmonth, + Trade.close_date < (profitmonth + relativedelta(months=1)) + ]).order_by(Trade.close_date).all() + curmonthprofit = sum( + trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) + profit_months[profitmonth] = { + 'amount': curmonthprofit, + 'trades': len(trades) + } + + data = [ + { + 'date': key, + 'abs_profit': value["amount"], + 'fiat_value': self._fiat_converter.convert_amount( + value['amount'], + stake_currency, + fiat_display_currency + ) if self._fiat_converter else 0, + 'trade_count': value["trades"], + } + for key, value in profit_months.items() + ] + return { + 'stake_currency': stake_currency, + 'fiat_display_currency': fiat_display_currency, + 'data': data + } + def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ order_by = Trade.id if order_by_id else Trade.close_date.desc() diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 059ba9c41..6ceb3cbdb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -154,6 +154,8 @@ class Telegram(RPCHandler): CommandHandler('performance', self._performance), CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), + CommandHandler('weekly', self._weekly), + CommandHandler('monthly', self._monthly), CommandHandler('count', self._count), CommandHandler('locks', self._locks), CommandHandler(['unlock', 'delete_locks'], self._delete_locks), @@ -170,6 +172,7 @@ class Telegram(RPCHandler): callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), CallbackQueryHandler(self._daily, pattern='update_daily'), + CallbackQueryHandler(self._monthly, pattern='update_monthly'), CallbackQueryHandler(self._profit, pattern='update_profit'), CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), @@ -478,7 +481,7 @@ class Telegram(RPCHandler): f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ - 'Day', + 'Month', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}', 'Trades', @@ -490,6 +493,84 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _weekly(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /weekly + Returns a weekly profit (in BTC) over the last n weeks. + :param bot: telegram bot + :param update: message update + :return: None + """ + stake_cur = self._config['stake_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') + try: + timescale = int(context.args[0]) if context.args else 8 + except (TypeError, ValueError, IndexError): + timescale = 8 + try: + stats = self._rpc._rpc_weekly_profit( + timescale, + stake_cur, + fiat_disp_cur + ) + stats_tab = tabulate( + [[week['date'], + f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}", + f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}", + f"{week['trade_count']} trades"] for week in stats['data']], + headers=[ + 'Week', + f'Profit {stake_cur}', + f'Profit {fiat_disp_cur}', + 'Trades', + ], + tablefmt='simple') + message = f'Weekly Profit over the last {timescale} weeks:\n
{stats_tab}
' + self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path="update_weekly", query=update.callback_query) + except RPCException as e: + self._send_msg(str(e)) + + @authorized_only + def _monthly(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /monthly + Returns a monthly profit (in BTC) over the last n months. + :param bot: telegram bot + :param update: message update + :return: None + """ + stake_cur = self._config['stake_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') + try: + timescale = int(context.args[0]) if context.args else 6 + except (TypeError, ValueError, IndexError): + timescale = 6 + try: + stats = self._rpc._rpc_monthly_profit( + timescale, + stake_cur, + fiat_disp_cur + ) + stats_tab = tabulate( + [[month['date'], + f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}", + f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}", + f"{month['trade_count']} trades"] for month in stats['data']], + headers=[ + 'Month', + f'Profit {stake_cur}', + f'Profit {fiat_disp_cur}', + 'Trades', + ], + tablefmt='simple') + message = f'Monthly Profit over the last {timescale} months:\n
{stats_tab}
' + self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path="update_monthly", query=update.callback_query) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _profit(self, update: Update, context: CallbackContext) -> None: """ diff --git a/requirements.txt b/requirements.txt index 7fe06b9d2..53f73557a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,3 +43,5 @@ colorama==0.4.4 # Building config files interactively questionary==1.10.0 prompt-toolkit==3.0.20 + +python-dateutil==2.8.2 \ No newline at end of file diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7dde7b803..ae9bbe892 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -4,7 +4,8 @@ import logging import re -from datetime import datetime +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta from functools import reduce from random import choice, randint from string import ascii_uppercase @@ -92,10 +93,11 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " - "['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], " - "['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" + "['delete'], ['performance'], ['stats'], ['daily'], ['weekly'], ['monthly'], " + "['count'], ['locks'], ['unlock', 'delete_locks'], " + "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " + "['stopbuy'], ['whitelist'], ['blacklist'], " + "['logs'], ['edge'], ['help'], ['version']" "]") assert log_has(message_str, caplog) @@ -423,6 +425,239 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0] +def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, + limit_sell_order, mocker) -> None: + default_conf['max_open_trades'] = 1 + mocker.patch( + 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', + return_value=15000.0 + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + + # Try valid data + # /weekly 2 + context = MagicMock() + context.args = ["2"] + telegram._weekly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Weekly' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + context.args = [] + telegram._weekly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Weekly' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + freqtradebot.config['max_open_trades'] = 2 + # Add two other trades + n = freqtradebot.enter_positions() + assert n == 2 + + trades = Trade.query.all() + for trade in trades: + trade.update(limit_buy_order) + trade.update(limit_sell_order) + trade.close_date = datetime.utcnow() + trade.is_open = False + + # Make like the first trade was open and closed more than 8 weeks ago + trades[0].open_date = datetime.utcnow() - timedelta(weeks=8, days=2) + trades[0].close_date = datetime.utcnow() - timedelta(weeks=8, days=1) + + # /weekly + # By default, the 8 previous weeks are shown + # So the previous modified trade should be excluded from the stats + context = MagicMock() + context.args = [] + telegram._weekly(update=update, context=context) + # assert str(' 0.00012434 BTC') in msg_mock.call_args_list[0][0][0] + # assert str(' 1.865 USD') in msg_mock.call_args_list[0][0][0] + # assert str(' 2 trades') in msg_mock.call_args_list[0][0][0] + + # The time-shifted trade should not appear + assert str(' 0.00006217 BTC') not in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') not in msg_mock.call_args_list[0][0][0] + assert str(' 1 trades') not in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + context.args = ["10"] + telegram._weekly(update=update, context=context) + assert msg_mock.call_count == 1 + + a = msg_mock.call_args_list[0][0][0] + # Now, the time-shifted trade should be included + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trades') in msg_mock.call_args_list[0][0][0] + + +def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot) + + # Try invalid data + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + # /daily -2 + context = MagicMock() + context.args = ["-3"] + telegram._weekly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] + + # Try invalid data + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + # /daily today + context = MagicMock() + context.args = ["this week"] + telegram._weekly(update=update, context=context) + assert str('Weekly Profit over the last 8 weeks') in msg_mock.call_args_list[0][0][0] + + +def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, + limit_sell_order, mocker) -> None: + default_conf['max_open_trades'] = 1 + mocker.patch( + 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', + return_value=15000.0 + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + + # Try valid data + # /monthly 2 + context = MagicMock() + context.args = ["2"] + telegram._monthly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Monthly' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + context.args = [] + telegram._monthly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Monthly' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + freqtradebot.config['max_open_trades'] = 2 + # Add two other trades + n = freqtradebot.enter_positions() + assert n == 2 + + trades = Trade.query.all() + for trade in trades: + trade.update(limit_buy_order) + trade.update(limit_sell_order) + trade.close_date = datetime.utcnow() + trade.is_open = False + + # Make like the first trade was open and closed more than 6 months ago + trades[0].open_date = datetime.utcnow() - relativedelta(months=6, days=5) + trades[0].close_date = datetime.utcnow() - relativedelta(months=6, days=3) + + # /weekly + # By default, the 6 previous months are shown + # So the previous modified trade should be excluded from the stats + context = MagicMock() + context.args = [] + telegram._monthly(update=update, context=context) + # assert str(' 0.00012434 BTC') in msg_mock.call_args_list[0][0][0] + # assert str(' 1.865 USD') in msg_mock.call_args_list[0][0][0] + # assert str(' 2 trades') in msg_mock.call_args_list[0][0][0] + + # The time-shifted trade should not appear + assert str(' 0.00006217 BTC') not in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') not in msg_mock.call_args_list[0][0][0] + assert str(' 1 trades') not in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + context.args = ["8"] + telegram._monthly(update=update, context=context) + assert msg_mock.call_count == 1 + + a = msg_mock.call_args_list[0][0][0] + # Now, the time-shifted trade should be included + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trades') in msg_mock.call_args_list[0][0][0] + + def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) From 2115a3ed125c2dc1310f8b382b5933cd6e32c0df Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Fri, 5 Nov 2021 18:49:10 +0100 Subject: [PATCH 02/76] Update warning message open trades This shouldn't confuse user when just reloading their bot. --- freqtrade/freqtradebot.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d23ba270d..8ce3930ef 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -193,21 +193,22 @@ class FreqtradeBot(LoggingMixin): def check_for_open_trades(self): """ - Notify the user when the bot is stopped + Notify the user when the bot is stopped (not reloaded) and there are still open trades active. """ open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all() if len(open_trades) != 0: - msg = { - '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' " - f"to handle open trades gracefully. \n" - f"{'Trades are simulated.' if self.config['dry_run'] else ''}", - } - self.rpc.send_msg(msg) + if self.state != State.RELOAD_CONFIG: + msg = { + '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' " + f"to handle open trades gracefully. \n" + f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}", + } + self.rpc.send_msg(msg) def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: """ From 15616d75ad8a4c4334fac60dc7160802b5ce83fa Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Fri, 5 Nov 2021 20:24:40 +0100 Subject: [PATCH 03/76] Add /weekly and /monthly to Telegram RPC /weekly now list weeks starting from monday instead of rolling weeks. /monthly now list months starting from the 1st. Signed-off-by: Antoine Merino --- freqtrade/rpc/rpc.py | 11 ++--- freqtrade/rpc/telegram.py | 10 +++-- requirements.txt | 2 + tests/rpc/test_rpc_telegram.py | 73 ++++++++++++++++++++++++++-------- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index cd2599fa4..13838502b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -4,12 +4,12 @@ This module contains class to define a RPC communications import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone -from dateutil.relativedelta import relativedelta from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union import arrow import psutil +from dateutil.relativedelta import relativedelta from numpy import NAN, inf, int64, mean from pandas import DataFrame @@ -294,13 +294,14 @@ class RPC: self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: today = datetime.utcnow().date() + first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday profit_weeks: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for week in range(0, timescale): - profitweek = today - timedelta(weeks=week) + profitweek = first_iso_day_of_week - timedelta(weeks=week) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitweek, @@ -335,14 +336,14 @@ class RPC: def _rpc_monthly_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: - today = datetime.utcnow().date() + first_day_of_month = datetime.utcnow().date().replace(day=1) profit_months: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for month in range(0, timescale): - profitmonth = today - relativedelta(months=month) + profitmonth = first_day_of_month - relativedelta(months=month) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitmonth, @@ -357,7 +358,7 @@ class RPC: data = [ { - 'date': key, + 'date': f"{key.year}-{key.month}", 'abs_profit': value["amount"], 'fiat_value': self._fiat_converter.convert_amount( value['amount'], diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6ceb3cbdb..ae45d609f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -481,7 +481,7 @@ class Telegram(RPCHandler): f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ - 'Month', + 'Day', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}', 'Trades', @@ -520,13 +520,14 @@ class Telegram(RPCHandler): f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{week['trade_count']} trades"] for week in stats['data']], headers=[ - 'Week', + 'Monday', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}', 'Trades', ], tablefmt='simple') - message = f'Weekly Profit over the last {timescale} weeks:\n
{stats_tab}
' + message = f'Weekly Profit over the last {timescale} weeks ' \ + f'(starting from Monday):\n
{stats_tab}
' self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, callback_path="update_weekly", query=update.callback_query) except RPCException as e: @@ -565,7 +566,8 @@ class Telegram(RPCHandler): 'Trades', ], tablefmt='simple') - message = f'Monthly Profit over the last {timescale} months:\n
{stats_tab}
' + message = f'Monthly Profit over the last {timescale} months' \ + f':\n
{stats_tab}
' self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, callback_path="update_monthly", query=update.callback_query) except RPCException as e: diff --git a/requirements.txt b/requirements.txt index 53f73557a..5cb464d35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -44,4 +44,6 @@ colorama==0.4.4 questionary==1.10.0 prompt-toolkit==3.0.20 +# Extensions to datetime library +types-python-dateutil==2.8.2 python-dateutil==2.8.2 \ No newline at end of file diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ae9bbe892..0e00a52ae 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -5,7 +5,6 @@ import logging import re from datetime import datetime, timedelta -from dateutil.relativedelta import relativedelta from functools import reduce from random import choice, randint from string import ascii_uppercase @@ -13,6 +12,7 @@ from unittest.mock import ANY, MagicMock import arrow import pytest +from dateutil.relativedelta import relativedelta from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import BadRequest, NetworkError, TelegramError @@ -354,7 +354,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["2"] telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Daily' in msg_mock.call_args_list[0][0][0] + assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] + assert 'Day ' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] @@ -366,7 +367,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = [] telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Daily' in msg_mock.call_args_list[0][0][0] + assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] @@ -422,7 +423,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: context = MagicMock() context.args = ["today"] telegram._daily(update=update, context=context) - assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0] + assert str('Daily Profit over the last 7 days:') in msg_mock.call_args_list[0][0][0] def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, @@ -462,8 +463,12 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["2"] telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Weekly' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert "Weekly Profit over the last 2 weeks (starting from Monday):" \ + in msg_mock.call_args_list[0][0][0] + assert 'Monday ' in msg_mock.call_args_list[0][0][0] + today = datetime.utcnow().date() + first_iso_day_of_current_week = today - timedelta(days=today.weekday()) + assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -474,8 +479,9 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = [] telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 + assert "Weekly Profit over the last 8 weeks (starting from Monday):" \ + in msg_mock.call_args_list[0][0][0] assert 'Weekly' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -519,8 +525,8 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["10"] telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 - - a = msg_mock.call_args_list[0][0][0] + assert "Weekly Profit over the last 10 weeks (starting from Monday):" \ + in msg_mock.call_args_list[0][0][0] # Now, the time-shifted trade should be included assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] @@ -549,11 +555,12 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None: # Try invalid data msg_mock.reset_mock() freqtradebot.state = State.RUNNING - # /daily today + # /weekly this week context = MagicMock() context.args = ["this week"] telegram._weekly(update=update, context=context) - assert str('Weekly Profit over the last 8 weeks') in msg_mock.call_args_list[0][0][0] + assert str('Weekly Profit over the last 8 weeks (starting from Monday):') \ + in msg_mock.call_args_list[0][0][0] def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, @@ -593,8 +600,11 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["2"] telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Monthly' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert 'Monthly Profit over the last 2 months:' in msg_mock.call_args_list[0][0][0] + assert 'Month ' in msg_mock.call_args_list[0][0][0] + today = datetime.utcnow().date() + current_month = f"{today.year}-{today.month} " + assert current_month in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -605,8 +615,9 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = [] telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Monthly' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] + assert 'Month ' in msg_mock.call_args_list[0][0][0] + assert current_month in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -630,7 +641,7 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, trades[0].open_date = datetime.utcnow() - relativedelta(months=6, days=5) trades[0].close_date = datetime.utcnow() - relativedelta(months=6, days=3) - # /weekly + # /monthly # By default, the 6 previous months are shown # So the previous modified trade should be excluded from the stats context = MagicMock() @@ -651,13 +662,41 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 - a = msg_mock.call_args_list[0][0][0] # Now, the time-shifted trade should be included assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trades') in msg_mock.call_args_list[0][0][0] +def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot) + + # Try invalid data + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + # /daily -2 + context = MagicMock() + context.args = ["-3"] + telegram._monthly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] + + # Try invalid data + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + # /monthly february + context = MagicMock() + context.args = ["february"] + telegram._monthly(update=update, context=context) + assert str('Monthly Profit over the last 6 months:') in msg_mock.call_args_list[0][0][0] + + def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) From a8651b0dcd70db946f5e5ba865154a5e4457902c Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Fri, 5 Nov 2021 20:44:01 +0100 Subject: [PATCH 04/76] /weekly and /monthly documentation Signed-off-by: Antoine Merino --- docs/telegram-usage.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index b9d01a236..8d38cce59 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -175,6 +175,8 @@ official commands. You can ask at any moment for help with `/help`. | `/performance` | Show performance of each finished trade grouped by pair | `/balance` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) +| `/weekly ` | Shows profit or loss per week, over the last n weeks (n defaults to 8) +| `/monthly ` | Shows profit or loss per month, over the last n months (n defaults to 6) | `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells | `/whitelist` | Show the current whitelist | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. @@ -307,10 +309,10 @@ Return the balance of all crypto-currency your have on the exchange. ### /daily -Per default `/daily` will return the 7 last days. -The example below if for `/daily 3`: +Per default `/daily` will return the 7 last days. The example below if for `/daily 3`: > **Daily Profit over the last 3 days:** + ``` Day Profit BTC Profit USD ---------- -------------- ------------ @@ -319,6 +321,36 @@ Day Profit BTC Profit USD 2018-01-01 0.00269130 BTC 34.986 USD ``` +### /weekly + +Per default `/weekly` will return the 8 last weeks, including the current week. Each week starts +from Monday. The example below if for `/weekly 3`: + +> **Weekly Profit over the last 3 weeks (starting from Monday):** + +``` +Monday Profit BTC Profit USD +---------- -------------- ------------ +2018-01-03 0.00224175 BTC 29,142 USD +2017-12-27 0.00033131 BTC 4,307 USD +2017-12-20 0.00269130 BTC 34.986 USD +``` + +### /monthly + +Per default `/monthly` will return the 6 last months, including the current month. The example below +if for `/monthly 3`: + +> **Monthly Profit over the last 3 months:** + +``` +Month Profit BTC Profit USD +---------- -------------- ------------ +2018-01 0.00224175 BTC 29,142 USD +2017-12 0.00033131 BTC 4,307 USD +2017-11 0.00269130 BTC 34.986 USD +``` + ### /whitelist Shows the current whitelist From 5f40158c0b5982d3baa263fe0bfd4f9018e5fef9 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Thu, 4 Nov 2021 20:47:01 +0100 Subject: [PATCH 05/76] WIP Add /weekly and /monthly to Telegram RPC Related to "Show average profit in overview" (#5527) Signed-off-by: Antoine Merino --- freqtrade/rpc/rpc.py | 85 ++++++++++++ freqtrade/rpc/telegram.py | 83 ++++++++++- requirements.txt | 2 + tests/rpc/test_rpc_telegram.py | 245 ++++++++++++++++++++++++++++++++- 4 files changed, 409 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index da8d23b7a..223abc14d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -4,6 +4,7 @@ This module contains class to define a RPC communications import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone +from dateutil.relativedelta import relativedelta from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union @@ -289,6 +290,90 @@ class RPC: 'data': data } + def _rpc_weekly_profit( + self, timescale: int, + stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: + today = datetime.utcnow().date() + profit_weeks: Dict[date, Dict] = {} + + if not (isinstance(timescale, int) and timescale > 0): + raise RPCException('timescale must be an integer greater than 0') + + for week in range(0, timescale): + profitweek = today - timedelta(weeks=week) + trades = Trade.get_trades(trade_filter=[ + Trade.is_open.is_(False), + Trade.close_date >= profitweek, + Trade.close_date < (profitweek + timedelta(weeks=1)) + ]).order_by(Trade.close_date).all() + curweekprofit = sum( + trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) + profit_weeks[profitweek] = { + 'amount': curweekprofit, + 'trades': len(trades) + } + + data = [ + { + 'date': key, + 'abs_profit': value["amount"], + 'fiat_value': self._fiat_converter.convert_amount( + value['amount'], + stake_currency, + fiat_display_currency + ) if self._fiat_converter else 0, + 'trade_count': value["trades"], + } + for key, value in profit_weeks.items() + ] + return { + 'stake_currency': stake_currency, + 'fiat_display_currency': fiat_display_currency, + 'data': data + } + + def _rpc_monthly_profit( + self, timescale: int, + stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: + today = datetime.utcnow().date() + profit_months: Dict[date, Dict] = {} + + if not (isinstance(timescale, int) and timescale > 0): + raise RPCException('timescale must be an integer greater than 0') + + for month in range(0, timescale): + profitmonth = today - relativedelta(months=month) + trades = Trade.get_trades(trade_filter=[ + Trade.is_open.is_(False), + Trade.close_date >= profitmonth, + Trade.close_date < (profitmonth + relativedelta(months=1)) + ]).order_by(Trade.close_date).all() + curmonthprofit = sum( + trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) + profit_months[profitmonth] = { + 'amount': curmonthprofit, + 'trades': len(trades) + } + + data = [ + { + 'date': key, + 'abs_profit': value["amount"], + 'fiat_value': self._fiat_converter.convert_amount( + value['amount'], + stake_currency, + fiat_display_currency + ) if self._fiat_converter else 0, + 'trade_count': value["trades"], + } + for key, value in profit_months.items() + ] + return { + 'stake_currency': stake_currency, + 'fiat_display_currency': fiat_display_currency, + 'data': data + } + def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ order_by = Trade.id if order_by_id else Trade.close_date.desc() diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 77d5f06e2..32324a349 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -159,6 +159,8 @@ class Telegram(RPCHandler): CommandHandler('mix_tags', self._mix_tag_performance), CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), + CommandHandler('weekly', self._weekly), + CommandHandler('monthly', self._monthly), CommandHandler('count', self._count), CommandHandler('locks', self._locks), CommandHandler(['unlock', 'delete_locks'], self._delete_locks), @@ -175,6 +177,7 @@ class Telegram(RPCHandler): callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), CallbackQueryHandler(self._daily, pattern='update_daily'), + CallbackQueryHandler(self._monthly, pattern='update_monthly'), CallbackQueryHandler(self._profit, pattern='update_profit'), CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), @@ -489,7 +492,7 @@ class Telegram(RPCHandler): f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ - 'Day', + 'Month', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}', 'Trades', @@ -501,6 +504,84 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _weekly(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /weekly + Returns a weekly profit (in BTC) over the last n weeks. + :param bot: telegram bot + :param update: message update + :return: None + """ + stake_cur = self._config['stake_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') + try: + timescale = int(context.args[0]) if context.args else 8 + except (TypeError, ValueError, IndexError): + timescale = 8 + try: + stats = self._rpc._rpc_weekly_profit( + timescale, + stake_cur, + fiat_disp_cur + ) + stats_tab = tabulate( + [[week['date'], + f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}", + f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}", + f"{week['trade_count']} trades"] for week in stats['data']], + headers=[ + 'Week', + f'Profit {stake_cur}', + f'Profit {fiat_disp_cur}', + 'Trades', + ], + tablefmt='simple') + message = f'Weekly Profit over the last {timescale} weeks:\n
{stats_tab}
' + self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path="update_weekly", query=update.callback_query) + except RPCException as e: + self._send_msg(str(e)) + + @authorized_only + def _monthly(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /monthly + Returns a monthly profit (in BTC) over the last n months. + :param bot: telegram bot + :param update: message update + :return: None + """ + stake_cur = self._config['stake_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') + try: + timescale = int(context.args[0]) if context.args else 6 + except (TypeError, ValueError, IndexError): + timescale = 6 + try: + stats = self._rpc._rpc_monthly_profit( + timescale, + stake_cur, + fiat_disp_cur + ) + stats_tab = tabulate( + [[month['date'], + f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}", + f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}", + f"{month['trade_count']} trades"] for month in stats['data']], + headers=[ + 'Month', + f'Profit {stake_cur}', + f'Profit {fiat_disp_cur}', + 'Trades', + ], + tablefmt='simple') + message = f'Monthly Profit over the last {timescale} months:\n
{stats_tab}
' + self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path="update_monthly", query=update.callback_query) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _profit(self, update: Update, context: CallbackContext) -> None: """ diff --git a/requirements.txt b/requirements.txt index e2f6a3162..8476cb664 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,3 +42,5 @@ colorama==0.4.4 # Building config files interactively questionary==1.10.0 prompt-toolkit==3.0.21 + +python-dateutil==2.8.2 \ No newline at end of file diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5f49c8bf7..896b14ecf 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -4,7 +4,8 @@ import logging import re -from datetime import datetime +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta from functools import reduce from random import choice, randint from string import ascii_uppercase @@ -94,10 +95,11 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['delete'], ['performance'], ['buys'], ['sells'], ['mix_tags'], " - "['stats'], ['daily'], ['count'], ['locks'], " - "['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], " - "['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" + "['stats'], ['daily'], ['weekly'], ['monthly'], " + "['count'], ['locks'], ['unlock', 'delete_locks'], " + "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " + "['stopbuy'], ['whitelist'], ['blacklist'], " + "['logs'], ['edge'], ['help'], ['version']" "]") assert log_has(message_str, caplog) @@ -425,6 +427,239 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0] +def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, + limit_sell_order, mocker) -> None: + default_conf['max_open_trades'] = 1 + mocker.patch( + 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', + return_value=15000.0 + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + + # Try valid data + # /weekly 2 + context = MagicMock() + context.args = ["2"] + telegram._weekly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Weekly' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + context.args = [] + telegram._weekly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Weekly' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + freqtradebot.config['max_open_trades'] = 2 + # Add two other trades + n = freqtradebot.enter_positions() + assert n == 2 + + trades = Trade.query.all() + for trade in trades: + trade.update(limit_buy_order) + trade.update(limit_sell_order) + trade.close_date = datetime.utcnow() + trade.is_open = False + + # Make like the first trade was open and closed more than 8 weeks ago + trades[0].open_date = datetime.utcnow() - timedelta(weeks=8, days=2) + trades[0].close_date = datetime.utcnow() - timedelta(weeks=8, days=1) + + # /weekly + # By default, the 8 previous weeks are shown + # So the previous modified trade should be excluded from the stats + context = MagicMock() + context.args = [] + telegram._weekly(update=update, context=context) + # assert str(' 0.00012434 BTC') in msg_mock.call_args_list[0][0][0] + # assert str(' 1.865 USD') in msg_mock.call_args_list[0][0][0] + # assert str(' 2 trades') in msg_mock.call_args_list[0][0][0] + + # The time-shifted trade should not appear + assert str(' 0.00006217 BTC') not in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') not in msg_mock.call_args_list[0][0][0] + assert str(' 1 trades') not in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + context.args = ["10"] + telegram._weekly(update=update, context=context) + assert msg_mock.call_count == 1 + + a = msg_mock.call_args_list[0][0][0] + # Now, the time-shifted trade should be included + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trades') in msg_mock.call_args_list[0][0][0] + + +def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot) + + # Try invalid data + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + # /daily -2 + context = MagicMock() + context.args = ["-3"] + telegram._weekly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] + + # Try invalid data + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + # /daily today + context = MagicMock() + context.args = ["this week"] + telegram._weekly(update=update, context=context) + assert str('Weekly Profit over the last 8 weeks') in msg_mock.call_args_list[0][0][0] + + +def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, + limit_sell_order, mocker) -> None: + default_conf['max_open_trades'] = 1 + mocker.patch( + 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', + return_value=15000.0 + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + + # Try valid data + # /monthly 2 + context = MagicMock() + context.args = ["2"] + telegram._monthly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Monthly' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + context.args = [] + telegram._monthly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Monthly' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + freqtradebot.config['max_open_trades'] = 2 + # Add two other trades + n = freqtradebot.enter_positions() + assert n == 2 + + trades = Trade.query.all() + for trade in trades: + trade.update(limit_buy_order) + trade.update(limit_sell_order) + trade.close_date = datetime.utcnow() + trade.is_open = False + + # Make like the first trade was open and closed more than 6 months ago + trades[0].open_date = datetime.utcnow() - relativedelta(months=6, days=5) + trades[0].close_date = datetime.utcnow() - relativedelta(months=6, days=3) + + # /weekly + # By default, the 6 previous months are shown + # So the previous modified trade should be excluded from the stats + context = MagicMock() + context.args = [] + telegram._monthly(update=update, context=context) + # assert str(' 0.00012434 BTC') in msg_mock.call_args_list[0][0][0] + # assert str(' 1.865 USD') in msg_mock.call_args_list[0][0][0] + # assert str(' 2 trades') in msg_mock.call_args_list[0][0][0] + + # The time-shifted trade should not appear + assert str(' 0.00006217 BTC') not in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') not in msg_mock.call_args_list[0][0][0] + assert str(' 1 trades') not in msg_mock.call_args_list[0][0][0] + + # Reset msg_mock + msg_mock.reset_mock() + context.args = ["8"] + telegram._monthly(update=update, context=context) + assert msg_mock.call_count == 1 + + a = msg_mock.call_args_list[0][0][0] + # Now, the time-shifted trade should be included + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trades') in msg_mock.call_args_list[0][0][0] + + def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) From 459ff9692d4c8fdfe039c5f3dce4aab5b49ea3de Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Fri, 5 Nov 2021 20:24:40 +0100 Subject: [PATCH 06/76] Add /weekly and /monthly to Telegram RPC /weekly now list weeks starting from monday instead of rolling weeks. /monthly now list months starting from the 1st. Signed-off-by: Antoine Merino --- freqtrade/rpc/rpc.py | 11 ++--- freqtrade/rpc/telegram.py | 10 +++-- requirements.txt | 2 + tests/rpc/test_rpc_telegram.py | 73 ++++++++++++++++++++++++++-------- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 223abc14d..72042b247 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -4,12 +4,12 @@ This module contains class to define a RPC communications import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone -from dateutil.relativedelta import relativedelta from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union import arrow import psutil +from dateutil.relativedelta import relativedelta from numpy import NAN, inf, int64, mean from pandas import DataFrame @@ -294,13 +294,14 @@ class RPC: self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: today = datetime.utcnow().date() + first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday profit_weeks: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for week in range(0, timescale): - profitweek = today - timedelta(weeks=week) + profitweek = first_iso_day_of_week - timedelta(weeks=week) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitweek, @@ -335,14 +336,14 @@ class RPC: def _rpc_monthly_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: - today = datetime.utcnow().date() + first_day_of_month = datetime.utcnow().date().replace(day=1) profit_months: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for month in range(0, timescale): - profitmonth = today - relativedelta(months=month) + profitmonth = first_day_of_month - relativedelta(months=month) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitmonth, @@ -357,7 +358,7 @@ class RPC: data = [ { - 'date': key, + 'date': f"{key.year}-{key.month}", 'abs_profit': value["amount"], 'fiat_value': self._fiat_converter.convert_amount( value['amount'], diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 32324a349..962d67436 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -492,7 +492,7 @@ class Telegram(RPCHandler): f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ - 'Month', + 'Day', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}', 'Trades', @@ -531,13 +531,14 @@ class Telegram(RPCHandler): f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{week['trade_count']} trades"] for week in stats['data']], headers=[ - 'Week', + 'Monday', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}', 'Trades', ], tablefmt='simple') - message = f'Weekly Profit over the last {timescale} weeks:\n
{stats_tab}
' + message = f'Weekly Profit over the last {timescale} weeks ' \ + f'(starting from Monday):\n
{stats_tab}
' self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, callback_path="update_weekly", query=update.callback_query) except RPCException as e: @@ -576,7 +577,8 @@ class Telegram(RPCHandler): 'Trades', ], tablefmt='simple') - message = f'Monthly Profit over the last {timescale} months:\n
{stats_tab}
' + message = f'Monthly Profit over the last {timescale} months' \ + f':\n
{stats_tab}
' self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, callback_path="update_monthly", query=update.callback_query) except RPCException as e: diff --git a/requirements.txt b/requirements.txt index 8476cb664..733f1d028 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,4 +43,6 @@ colorama==0.4.4 questionary==1.10.0 prompt-toolkit==3.0.21 +# Extensions to datetime library +types-python-dateutil==2.8.2 python-dateutil==2.8.2 \ No newline at end of file diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 896b14ecf..627e184b1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -5,7 +5,6 @@ import logging import re from datetime import datetime, timedelta -from dateutil.relativedelta import relativedelta from functools import reduce from random import choice, randint from string import ascii_uppercase @@ -13,6 +12,7 @@ from unittest.mock import ANY, MagicMock import arrow import pytest +from dateutil.relativedelta import relativedelta from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import BadRequest, NetworkError, TelegramError @@ -356,7 +356,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["2"] telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Daily' in msg_mock.call_args_list[0][0][0] + assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] + assert 'Day ' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] @@ -368,7 +369,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = [] telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Daily' in msg_mock.call_args_list[0][0][0] + assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] @@ -424,7 +425,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: context = MagicMock() context.args = ["today"] telegram._daily(update=update, context=context) - assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0] + assert str('Daily Profit over the last 7 days:') in msg_mock.call_args_list[0][0][0] def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, @@ -464,8 +465,12 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["2"] telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Weekly' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert "Weekly Profit over the last 2 weeks (starting from Monday):" \ + in msg_mock.call_args_list[0][0][0] + assert 'Monday ' in msg_mock.call_args_list[0][0][0] + today = datetime.utcnow().date() + first_iso_day_of_current_week = today - timedelta(days=today.weekday()) + assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -476,8 +481,9 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = [] telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 + assert "Weekly Profit over the last 8 weeks (starting from Monday):" \ + in msg_mock.call_args_list[0][0][0] assert 'Weekly' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -521,8 +527,8 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["10"] telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 - - a = msg_mock.call_args_list[0][0][0] + assert "Weekly Profit over the last 10 weeks (starting from Monday):" \ + in msg_mock.call_args_list[0][0][0] # Now, the time-shifted trade should be included assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] @@ -551,11 +557,12 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None: # Try invalid data msg_mock.reset_mock() freqtradebot.state = State.RUNNING - # /daily today + # /weekly this week context = MagicMock() context.args = ["this week"] telegram._weekly(update=update, context=context) - assert str('Weekly Profit over the last 8 weeks') in msg_mock.call_args_list[0][0][0] + assert str('Weekly Profit over the last 8 weeks (starting from Monday):') \ + in msg_mock.call_args_list[0][0][0] def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, @@ -595,8 +602,11 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["2"] telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Monthly' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert 'Monthly Profit over the last 2 months:' in msg_mock.call_args_list[0][0][0] + assert 'Month ' in msg_mock.call_args_list[0][0][0] + today = datetime.utcnow().date() + current_month = f"{today.year}-{today.month} " + assert current_month in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -607,8 +617,9 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = [] telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 - assert 'Monthly' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] + assert 'Month ' in msg_mock.call_args_list[0][0][0] + assert current_month in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -632,7 +643,7 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, trades[0].open_date = datetime.utcnow() - relativedelta(months=6, days=5) trades[0].close_date = datetime.utcnow() - relativedelta(months=6, days=3) - # /weekly + # /monthly # By default, the 6 previous months are shown # So the previous modified trade should be excluded from the stats context = MagicMock() @@ -653,13 +664,41 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 - a = msg_mock.call_args_list[0][0][0] # Now, the time-shifted trade should be included assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trades') in msg_mock.call_args_list[0][0][0] +def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot) + + # Try invalid data + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + # /daily -2 + context = MagicMock() + context.args = ["-3"] + telegram._monthly(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] + + # Try invalid data + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + # /monthly february + context = MagicMock() + context.args = ["february"] + telegram._monthly(update=update, context=context) + assert str('Monthly Profit over the last 6 months:') in msg_mock.call_args_list[0][0][0] + + def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) From 87634f040907c8ab05983b9331e9f3e2c33c4138 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Fri, 5 Nov 2021 20:44:01 +0100 Subject: [PATCH 07/76] /weekly and /monthly documentation Signed-off-by: Antoine Merino --- docs/telegram-usage.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 0c45fbbf1..3f2d062fc 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -175,6 +175,8 @@ official commands. You can ask at any moment for help with `/help`. | `/performance` | Show performance of each finished trade grouped by pair | `/balance` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) +| `/weekly ` | Shows profit or loss per week, over the last n weeks (n defaults to 8) +| `/monthly ` | Shows profit or loss per month, over the last n months (n defaults to 6) | `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells | `/whitelist` | Show the current whitelist | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. @@ -307,10 +309,10 @@ Return the balance of all crypto-currency your have on the exchange. ### /daily -Per default `/daily` will return the 7 last days. -The example below if for `/daily 3`: +Per default `/daily` will return the 7 last days. The example below if for `/daily 3`: > **Daily Profit over the last 3 days:** + ``` Day Profit BTC Profit USD ---------- -------------- ------------ @@ -319,6 +321,36 @@ Day Profit BTC Profit USD 2018-01-01 0.00269130 BTC 34.986 USD ``` +### /weekly + +Per default `/weekly` will return the 8 last weeks, including the current week. Each week starts +from Monday. The example below if for `/weekly 3`: + +> **Weekly Profit over the last 3 weeks (starting from Monday):** + +``` +Monday Profit BTC Profit USD +---------- -------------- ------------ +2018-01-03 0.00224175 BTC 29,142 USD +2017-12-27 0.00033131 BTC 4,307 USD +2017-12-20 0.00269130 BTC 34.986 USD +``` + +### /monthly + +Per default `/monthly` will return the 6 last months, including the current month. The example below +if for `/monthly 3`: + +> **Monthly Profit over the last 3 months:** + +``` +Month Profit BTC Profit USD +---------- -------------- ------------ +2018-01 0.00224175 BTC 29,142 USD +2017-12 0.00033131 BTC 4,307 USD +2017-11 0.00269130 BTC 34.986 USD +``` + ### /whitelist Shows the current whitelist From 70253258f04b146f9e5675aa15784e6c517c28f9 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Fri, 5 Nov 2021 22:33:06 +0100 Subject: [PATCH 08/76] Test /monthly & clean Signed-off-by: Antoine Merino --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc_telegram.py | 71 ++++++++-------------------------- 2 files changed, 18 insertions(+), 55 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 72042b247..3cdcdf70a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -358,7 +358,7 @@ class RPC: data = [ { - 'date': f"{key.year}-{key.month}", + 'date': f"{key.year}-{key.month:02d}", 'abs_profit': value["amount"], 'fiat_value': self._fiat_converter.convert_amount( value['amount'], diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 627e184b1..74c33d2af 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -12,7 +12,6 @@ from unittest.mock import ANY, MagicMock import arrow import pytest -from dateutil.relativedelta import relativedelta from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import BadRequest, NetworkError, TelegramError @@ -503,36 +502,15 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, trade.close_date = datetime.utcnow() trade.is_open = False - # Make like the first trade was open and closed more than 8 weeks ago - trades[0].open_date = datetime.utcnow() - timedelta(weeks=8, days=2) - trades[0].close_date = datetime.utcnow() - timedelta(weeks=8, days=1) - - # /weekly + # /weekly 1 # By default, the 8 previous weeks are shown # So the previous modified trade should be excluded from the stats context = MagicMock() - context.args = [] + context.args = ["1"] telegram._weekly(update=update, context=context) - # assert str(' 0.00012434 BTC') in msg_mock.call_args_list[0][0][0] - # assert str(' 1.865 USD') in msg_mock.call_args_list[0][0][0] - # assert str(' 2 trades') in msg_mock.call_args_list[0][0][0] - - # The time-shifted trade should not appear - assert str(' 0.00006217 BTC') not in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') not in msg_mock.call_args_list[0][0][0] - assert str(' 1 trades') not in msg_mock.call_args_list[0][0][0] - - # Reset msg_mock - msg_mock.reset_mock() - context.args = ["10"] - telegram._weekly(update=update, context=context) - assert msg_mock.call_count == 1 - assert "Weekly Profit over the last 10 weeks (starting from Monday):" \ - in msg_mock.call_args_list[0][0][0] - # Now, the time-shifted trade should be included - assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 1 trades') in msg_mock.call_args_list[0][0][0] + assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None: @@ -547,7 +525,7 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None: # Try invalid data msg_mock.reset_mock() freqtradebot.state = State.RUNNING - # /daily -2 + # /weekly -3 context = MagicMock() context.args = ["-3"] telegram._weekly(update=update, context=context) @@ -617,6 +595,7 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = [] telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 + # Default to 6 months assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] assert 'Month ' in msg_mock.call_args_list[0][0][0] assert current_month in msg_mock.call_args_list[0][0][0] @@ -639,35 +618,19 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, trade.close_date = datetime.utcnow() trade.is_open = False - # Make like the first trade was open and closed more than 6 months ago - trades[0].open_date = datetime.utcnow() - relativedelta(months=6, days=5) - trades[0].close_date = datetime.utcnow() - relativedelta(months=6, days=3) - - # /monthly - # By default, the 6 previous months are shown - # So the previous modified trade should be excluded from the stats + # /monthly 12 context = MagicMock() - context.args = [] - telegram._monthly(update=update, context=context) - # assert str(' 0.00012434 BTC') in msg_mock.call_args_list[0][0][0] - # assert str(' 1.865 USD') in msg_mock.call_args_list[0][0][0] - # assert str(' 2 trades') in msg_mock.call_args_list[0][0][0] - - # The time-shifted trade should not appear - assert str(' 0.00006217 BTC') not in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') not in msg_mock.call_args_list[0][0][0] - assert str(' 1 trades') not in msg_mock.call_args_list[0][0][0] - - # Reset msg_mock - msg_mock.reset_mock() - context.args = ["8"] + context.args = ["12"] telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 + assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] + assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] - # Now, the time-shifted trade should be included - assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 1 trades') in msg_mock.call_args_list[0][0][0] + # The one-digit months should contain a zero, Eg: September 2021 = "2021-09" + # Since we loaded the last 12 months, any month should appear + assert str('-09') in msg_mock.call_args_list[0][0][0] def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None: @@ -682,7 +645,7 @@ def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None: # Try invalid data msg_mock.reset_mock() freqtradebot.state = State.RUNNING - # /daily -2 + # /monthly -3 context = MagicMock() context.args = ["-3"] telegram._monthly(update=update, context=context) From 8eabdd659f2d018a9414681a32a0757bb23a7593 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Fri, 5 Nov 2021 22:51:35 +0100 Subject: [PATCH 09/76] Fix missing CallbackQueryHandler Signed-off-by: Antoine Merino --- freqtrade/rpc/telegram.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 962d67436..1dea5fae7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -177,6 +177,7 @@ class Telegram(RPCHandler): callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), CallbackQueryHandler(self._daily, pattern='update_daily'), + CallbackQueryHandler(self._weekly, pattern='update_weekly'), CallbackQueryHandler(self._monthly, pattern='update_monthly'), CallbackQueryHandler(self._profit, pattern='update_profit'), CallbackQueryHandler(self._balance, pattern='update_balance'), From da4344d2166c7e802af44ccdea6ed15ed39b4455 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Fri, 5 Nov 2021 22:52:04 +0100 Subject: [PATCH 10/76] Remove line breaks Signed-off-by: Antoine Merino --- docs/telegram-usage.md | 3 --- requirements.txt | 1 - 2 files changed, 4 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 3f2d062fc..0c1af9f4d 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -312,7 +312,6 @@ Return the balance of all crypto-currency your have on the exchange. Per default `/daily` will return the 7 last days. The example below if for `/daily 3`: > **Daily Profit over the last 3 days:** - ``` Day Profit BTC Profit USD ---------- -------------- ------------ @@ -327,7 +326,6 @@ Per default `/weekly` will return the 8 last weeks, including the current week. from Monday. The example below if for `/weekly 3`: > **Weekly Profit over the last 3 weeks (starting from Monday):** - ``` Monday Profit BTC Profit USD ---------- -------------- ------------ @@ -342,7 +340,6 @@ Per default `/monthly` will return the 6 last months, including the current mont if for `/monthly 3`: > **Monthly Profit over the last 3 months:** - ``` Month Profit BTC Profit USD ---------- -------------- ------------ diff --git a/requirements.txt b/requirements.txt index 733f1d028..8337d7bd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,7 +42,6 @@ colorama==0.4.4 # Building config files interactively questionary==1.10.0 prompt-toolkit==3.0.21 - # Extensions to datetime library types-python-dateutil==2.8.2 python-dateutil==2.8.2 \ No newline at end of file From 7a907a76362ab964cccd593d1970d3ab97116837 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Nov 2021 11:48:49 +0100 Subject: [PATCH 11/76] Add Emergencyselling after X timeouts have been reached --- config_examples/config_full.example.json | 1 + docs/configuration.md | 1 + freqtrade/constants.py | 1 + freqtrade/freqtradebot.py | 9 ++++++++- freqtrade/persistence/models.py | 10 +++++++++- 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 83b8a27d0..228a08a02 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -28,6 +28,7 @@ "unfilledtimeout": { "buy": 10, "sell": 30, + "exit_timeout_count": 0, "unit": "minutes" }, "bid_strategy": { diff --git a/docs/configuration.md b/docs/configuration.md index 24198b44c..c566e33c2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -101,6 +101,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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 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.exit_timeout_count` | How many times can exit orders time out. 0 to disable and allow unlimited cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**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). diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 656893999..73830af09 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,6 +156,7 @@ CONF_SCHEMA = { 'properties': { 'buy': {'type': 'number', 'minimum': 1}, 'sell': {'type': 'number', 'minimum': 1}, + 'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0}, 'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'} } }, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a1e8cad4a..a797b2e70 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -920,6 +920,13 @@ class FreqtradeBot(LoggingMixin): trade=trade, order=order))): self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) + canceled_count = trade.get_exit_order_count() + max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) + if max_timeouts > 0 and canceled_count >= max_timeouts: + logger.warning(f'Emergencyselling trade {trade}, as the sell order ' + f'timed out {max_timeouts} times.') + self.execute_trade_exit(trade, order.get('price'), sell_reason=SellCheckTuple( + sell_type=SellType.EMERGENCY_SELL)) def cancel_all_open_orders(self) -> None: """ @@ -1283,7 +1290,7 @@ class FreqtradeBot(LoggingMixin): if self.exchange.check_order_canceled_empty(order): # Trade has been cancelled on exchange - # Handling of this will happen in check_handle_timeout. + # Handling of this will happen in check_handle_timedout. return True # Try update amount (binance-fix) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b3518c228..bef04fd76 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -491,6 +491,14 @@ class LocalTrade(): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) + def get_exit_order_count(self) -> int: + """ + Get amount of failed exiting orders + assumes full exits. + """ + orders = [o for o in self.orders if o.ft_order_side == 'sell'] + return len(orders) + def _calc_open_trade_value(self) -> float: """ Calculate the open_rate including open_fee. @@ -775,7 +783,7 @@ class Trade(_DECL_BASE, LocalTrade): return Trade.query @staticmethod - def get_open_order_trades(): + def get_open_order_trades() -> List['Trade']: """ Returns all open trades NOTE: Not supported in Backtesting. From 3c33b48fd5fe21d195c17fd5948200c07920fe33 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Sat, 6 Nov 2021 13:09:15 +0100 Subject: [PATCH 12/76] Fix naive timezones --- freqtrade/rpc/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3cdcdf70a..58de3d1e8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -293,7 +293,7 @@ class RPC: def _rpc_weekly_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: - today = datetime.utcnow().date() + today = datetime.now(timezone.utc).date() first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday profit_weeks: Dict[date, Dict] = {} @@ -336,7 +336,7 @@ class RPC: def _rpc_monthly_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: - first_day_of_month = datetime.utcnow().date().replace(day=1) + first_day_of_month = datetime.now(timezone.utc).date().replace(day=1) profit_months: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): From d5acd979dc53afd0d9a9b6e28dc4cb643a1bb149 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Sat, 6 Nov 2021 13:10:22 +0100 Subject: [PATCH 13/76] Move dev-only requirement --- requirements-dev.txt | 3 +++ requirements.txt | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c2b62196c..d90ffcf78 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,3 +24,6 @@ types-cachetools==4.2.4 types-filelock==3.2.1 types-requests==2.25.11 types-tabulate==0.8.3 + +# Extensions to datetime library +types-python-dateutil==2.8.2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8337d7bd1..90cfe46d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,5 +43,4 @@ colorama==0.4.4 questionary==1.10.0 prompt-toolkit==3.0.21 # Extensions to datetime library -types-python-dateutil==2.8.2 python-dateutil==2.8.2 \ No newline at end of file From f7dc47b1c8b17f61b05477c579a3a52fe355943c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Nov 2021 13:10:41 +0100 Subject: [PATCH 14/76] Add test for exit_timeout_count --- freqtrade/persistence/models.py | 3 +-- tests/test_freqtradebot.py | 13 +++++++++++-- tests/test_persistence.py | 10 +++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index bef04fd76..61e59b7c3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -496,8 +496,7 @@ class LocalTrade(): Get amount of failed exiting orders assumes full exits. """ - orders = [o for o in self.orders if o.ft_order_side == 'sell'] - return len(orders) + return len([o for o in self.orders if o.ft_order_side == 'sell']) def _calc_open_trade_value(self) -> float: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5acefd515..47b76af61 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2134,11 +2134,12 @@ def test_check_handle_timedout_buy_exception(default_conf_usdt, ticker_usdt, def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old, - mocker, open_trade) -> None: - default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440} + mocker, open_trade, caplog) -> None: + default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1} rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) + et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, @@ -2181,6 +2182,14 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l assert open_trade.is_open is True assert freqtrade.strategy.check_sell_timeout.call_count == 1 + # 2nd canceled trade ... + caplog.clear() + open_trade.open_order_id = 'order_id_2' + mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1) + freqtrade.check_handle_timedout() + assert log_has_re('Emergencyselling trade.*', caplog) + assert et_mock.call_count == 1 + def test_check_handle_timedout_sell(default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, open_trade) -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 719dc8263..d1d3ce382 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -13,7 +13,7 @@ from sqlalchemy import create_engine, inspect, text from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db -from tests.conftest import create_mock_trades, log_has, log_has_re +from tests.conftest import create_mock_trades, create_mock_trades_usdt, log_has, log_has_re def test_init_create_session(default_conf): @@ -1190,6 +1190,14 @@ def test_get_best_pair(fee): assert res[1] == 0.01 +@pytest.mark.usefixtures("init_persistence") +def test_get_exit_order_count(fee): + + create_mock_trades_usdt(fee) + trade = Trade.get_trades([Trade.pair == 'ETC/USDT']).first() + assert trade.get_exit_order_count() == 1 + + @pytest.mark.usefixtures("init_persistence") def test_update_order_from_ccxt(caplog): # Most basic order return (only has orderid) From d0e192e20f54322c868c9ffaa4778286429d479b Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Sat, 6 Nov 2021 13:14:15 +0100 Subject: [PATCH 15/76] Fix naive timezone for /daily command --- 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 58de3d1e8..37ff80be5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -251,7 +251,7 @@ class RPC: def _rpc_daily_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: - today = datetime.utcnow().date() + today = datetime.now(timezone.utc).date() profit_days: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): From 4f5c5b6982bf21b6c72287c3270278c82e956903 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Nov 2021 15:29:05 +0100 Subject: [PATCH 16/76] Clarify timeout documentation --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index c566e33c2..f2958f725 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -101,8 +101,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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 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.exit_timeout_count` | How many times can exit orders time out. 0 to disable and allow unlimited cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**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 +| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**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.** 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 From 25fcab0794aec6973a633506553bffbbd4260a4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Nov 2021 16:12:25 +0100 Subject: [PATCH 17/76] Enhance /show_config endpoint --- freqtrade/constants.py | 5 ++++- freqtrade/rpc/api_server/api_schemas.py | 21 +++++++++++++++++++++ freqtrade/rpc/rpc.py | 4 ++++ tests/rpc/test_rpc_apiserver.py | 4 +++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 73830af09..e775e39fc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -207,7 +207,10 @@ CONF_SCHEMA = { 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, - 'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, + 'emergencysell': { + 'type': 'string', + 'enum': ORDERTYPE_POSSIBILITIES, + 'default': 'market'}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'}, 'stoploss_on_exchange_interval': {'type': 'number'}, diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index ff1915fca..5dbd1f637 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -123,7 +123,26 @@ class Daily(BaseModel): stake_currency: str +class UnfilledTimeout(BaseModel): + buy: int + sell: int + unit: str + exit_timeout_count: Optional[int] + + +class OrderTypes(BaseModel): + buy: str + sell: str + emergencysell: Optional[str] + forcesell: Optional[str] + forcebuy: Optional[str] + stoploss: str + stoploss_on_exchange: bool + stoploss_on_exchange_interval: Optional[int] + + class ShowConfig(BaseModel): + version: str dry_run: bool stake_currency: str stake_amount: Union[float, str] @@ -136,6 +155,8 @@ class ShowConfig(BaseModel): trailing_stop_positive: Optional[float] trailing_stop_positive_offset: Optional[float] trailing_only_offset_is_reached: Optional[bool] + unfilledtimeout: UnfilledTimeout + order_types: OrderTypes use_custom_stoploss: Optional[bool] timeframe: Optional[str] timeframe_ms: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 37ff80be5..97a0def0b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -13,6 +13,7 @@ from dateutil.relativedelta import relativedelta from numpy import NAN, inf, int64, mean from pandas import DataFrame +from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data @@ -104,6 +105,7 @@ class RPC: information via rpc. """ val = { + 'version': __version__, 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), @@ -117,7 +119,9 @@ class RPC: 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), + 'unfilledtimeout': config.get('unfilledtimeout'), 'use_custom_stoploss': config.get('use_custom_stoploss'), + 'order_types': config.get('order_types'), 'bot_name': config.get('bot_name', 'freqtrade'), 'timeframe': config.get('timeframe'), 'timeframe_ms': timeframe_to_msecs(config['timeframe'] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e0bbee861..3f908377b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -520,7 +520,7 @@ def test_api_locks(botclient): assert rc.json()['lock_count'] == 0 -def test_api_show_config(botclient, mocker): +def test_api_show_config(botclient): ftbot, client = botclient patch_get_signal(ftbot) @@ -536,6 +536,8 @@ def test_api_show_config(botclient, mocker): assert not rc.json()['trailing_stop'] assert 'bid_strategy' in rc.json() assert 'ask_strategy' in rc.json() + assert 'unfilledtimeout' in rc.json() + assert 'version' in rc.json() def test_api_daily(botclient, mocker, ticker, fee, markets): From 4595c1e73c08bf0a99d5ddf8d1084f091b17e688 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Nov 2021 10:55:11 +0100 Subject: [PATCH 18/76] Slightly reformat to simplify new change --- freqtrade/freqtradebot.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8ce3930ef..d29e6200d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -198,17 +198,17 @@ class FreqtradeBot(LoggingMixin): """ open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all() - if len(open_trades) != 0: - if self.state != State.RELOAD_CONFIG: - msg = { - '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' " - f"to handle open trades gracefully. \n" - f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}", - } - self.rpc.send_msg(msg) + if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG: + msg = { + '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' " + f"to handle open trades gracefully. \n" + f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}", + } + self.rpc.send_msg(msg) def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: """ From fb6ba621582f64ae2c77483c5d1212954f6b3d75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Nov 2021 19:52:29 +0100 Subject: [PATCH 19/76] Add default to "is_new_pair" --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/exchange.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8dced3894..06d64999d 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -93,7 +93,7 @@ class Binance(Exchange): raise OperationalException(e) from e async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, - since_ms: int, is_new_pair: bool + since_ms: int, is_new_pair: bool = False ) -> List: """ Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f9553699b..b22556f60 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1223,8 +1223,8 @@ class Exchange: drop_incomplete=self._ohlcv_partial_candle) async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, - since_ms: int, is_new_pair: bool - ) -> List: + since_ms: int, is_new_pair: bool = False + ) -> Tuple[str, str, List]: """ Download historic ohlcv :param is_new_pair: used by binance subclass to allow "fast" new pair downloading From 9fa64c264768c4156c7cfc84b5a86badd7ee62ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Nov 2021 20:00:02 +0100 Subject: [PATCH 20/76] Allow multiple calls to get more candles in live-run --- freqtrade/exchange/binance.py | 10 ++++---- freqtrade/exchange/exchange.py | 42 +++++++++++++++++++++++---------- tests/exchange/test_binance.py | 9 ++++--- tests/exchange/test_exchange.py | 8 ++++--- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 06d64999d..4ba30b626 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict, List +from typing import Dict, List, Tuple import arrow import ccxt @@ -93,8 +93,9 @@ class Binance(Exchange): raise OperationalException(e) from e async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, - since_ms: int, is_new_pair: bool = False - ) -> List: + since_ms: int, is_new_pair: bool = False, + raise_: bool = False + ) -> Tuple[str, str, List]: """ Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date Does not work for other exchanges, which don't return the earliest data when called with "0" @@ -107,4 +108,5 @@ class Binance(Exchange): logger.info(f"Candle-data for {pair} available starting with " f"{arrow.get(since_ms // 1000).isoformat()}.") return await super()._async_get_historic_ohlcv( - pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair) + pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair, + raise_=raise_) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b22556f60..01f0864c4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,7 +7,7 @@ import http import inspect import logging from copy import deepcopy -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from math import ceil from typing import Any, Dict, List, Optional, Tuple @@ -1205,9 +1205,11 @@ class Exchange: :param since_ms: Timestamp in milliseconds to get history from :return: List with candle (OHLCV) data """ - return asyncio.get_event_loop().run_until_complete( + pair, timeframe, data = asyncio.get_event_loop().run_until_complete( self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)) + logger.info(f"Downloaded data for {pair} with length {len(data)}.") + return data def get_historic_ohlcv_as_df(self, pair: str, timeframe: str, since_ms: int) -> DataFrame: @@ -1223,7 +1225,8 @@ class Exchange: drop_incomplete=self._ohlcv_partial_candle) async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, - since_ms: int, is_new_pair: bool = False + since_ms: int, is_new_pair: bool = False, + raise_: bool = False ) -> Tuple[str, str, List]: """ Download historic ohlcv @@ -1248,15 +1251,17 @@ class Exchange: for res in results: if isinstance(res, Exception): logger.warning("Async code raised an exception: %s", res.__class__.__name__) + if raise_: + raise continue - # Deconstruct tuple if it's not an exception - p, _, new_data = res - if p == pair: - data.extend(new_data) + else: + # Deconstruct tuple if it's not an exception + p, _, new_data = res + if p == pair: + data.extend(new_data) # Sort data again after extending the result - above calls return in "async order" data = sorted(data, key=lambda x: x[0]) - logger.info(f"Downloaded data for {pair} with length {len(data)}.") - return data + return pair, timeframe, data def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, since_ms: Optional[int] = None, cache: bool = True @@ -1276,10 +1281,23 @@ class Exchange: cached_pairs = [] # Gather coroutines to run for pair, timeframe in set(pair_list): - if (((pair, timeframe) not 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)) + call_count = self._ft_has.get('ohlcv_candle_call_count', 1) + if not since_ms and call_count > 1: + # Multiple calls for one pair - to get more history + one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) + move_to = one_call * call_count + now = timeframe_to_next_date(timeframe) + since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000) + + if since_ms: + input_coroutines.append(self._async_get_historic_ohlcv( + pair, timeframe, since_ms=since_ms, raise_=True)) + else: + # One call ... "regular" refresh + input_coroutines.append(self._async_get_candle_history( + pair, timeframe, since_ms=since_ms)) else: logger.debug( "Using cached candle (OHLCV) data for pair %s, timeframe %s ...", diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index dd85c3abe..d88ae9b1d 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -126,13 +126,16 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) pair = 'ETH/BTC' - res = await exchange._async_get_historic_ohlcv(pair, "5m", - 1500000000000, is_new_pair=False) + respair, restf, res = await exchange._async_get_historic_ohlcv( + pair, "5m", 1500000000000, is_new_pair=False) + assert respair == pair + assert restf == '5m' # Call with very old timestamp - causes tons of requests assert exchange._api_async.fetch_ohlcv.call_count > 400 # assert res == ohlcv exchange._api_async.fetch_ohlcv.reset_mock() - res = await exchange._async_get_historic_ohlcv(pair, "5m", 1500000000000, is_new_pair=True) + _, _, res = await exchange._async_get_historic_ohlcv( + pair, "5m", 1500000000000, is_new_pair=True) # Called twice - one "init" call - and one to get the actual data. assert exchange._api_async.fetch_ohlcv.call_count == 2 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e3369182d..34e2b04ab 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1506,6 +1506,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): assert exchange._async_get_candle_history.call_count == 2 # Returns twice the above OHLCV data assert len(ret) == 2 + assert log_has_re(r'Downloaded data for .* with length .*\.', caplog) caplog.clear() @@ -1587,12 +1588,13 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_ exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) pair = 'ETH/USDT' - res = await exchange._async_get_historic_ohlcv(pair, "5m", - 1500000000000, is_new_pair=False) + respair, restf, res = await exchange._async_get_historic_ohlcv( + pair, "5m", 1500000000000, is_new_pair=False) + assert respair == pair + assert restf == '5m' # Call with very old timestamp - causes tons of requests assert exchange._api_async.fetch_ohlcv.call_count > 200 assert res[0] == ohlcv[0] - assert log_has_re(r'Downloaded data for .* with length .*\.', caplog) def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: From a08dd17bc19c81156ace7735bad0cbb2fb98e2ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Nov 2021 13:10:40 +0100 Subject: [PATCH 21/76] Use startup_candle-count to determine call count --- freqtrade/exchange/exchange.py | 28 ++++++++++++++++++++-------- tests/exchange/test_exchange.py | 24 ++++++++++++++++++++---- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 01f0864c4..4e9a6c71d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -155,8 +155,8 @@ class Exchange: self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) - self.validate_required_startup_candles(config.get('startup_candle_count', 0), - config.get('timeframe', '')) + self.required_candle_call_count = self.validate_required_startup_candles( + config.get('startup_candle_count', 0), config.get('timeframe', '')) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -477,10 +477,23 @@ class Exchange: Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. """ candle_limit = self.ohlcv_candle_limit(timeframe) - if startup_candles + 5 > candle_limit: + # Require one more candle - to account for the still open candle. + candle_count = startup_candles + 1 + # Allow 5 calls to the exchange per pair + required_candle_call_count = int( + (candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1)) + + if required_candle_call_count > 5: + # Only allow 5 calls per pair to somewhat limit the impact raise OperationalException( - f"This strategy requires {startup_candles} candles to start. " - f"{self.name} only provides {candle_limit - 5} for {timeframe}.") + f"This strategy requires {startup_candles} candles to start, which is more than 5x " + f"the amount of candles {self.name} provides for {timeframe}.") + + if required_candle_call_count > 1: + logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. " + f"This can result in slower operations for the bot. Please check " + f"if you really need {startup_candles} candles for your strategy") + return required_candle_call_count def exchange_has(self, endpoint: str) -> bool: """ @@ -1283,11 +1296,10 @@ class Exchange: for pair, timeframe in set(pair_list): if ((pair, timeframe) not in self._klines or self._now_is_time_to_refresh(pair, timeframe)): - call_count = self._ft_has.get('ohlcv_candle_call_count', 1) - if not since_ms and call_count > 1: + if not since_ms and self.required_candle_call_count > 1: # Multiple calls for one pair - to get more history one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) - move_to = one_call * call_count + move_to = one_call * self.required_candle_call_count now = timeframe_to_next_date(timeframe) since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 34e2b04ab..ff78321c5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -941,12 +941,26 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): default_conf['startup_candle_count'] = 20 ex = Exchange(default_conf) assert ex - default_conf['startup_candle_count'] = 600 + # assumption is that the exchange provides 500 candles per call.s + assert ex.validate_required_startup_candles(200, '5m') == 1 + assert ex.validate_required_startup_candles(499, '5m') == 1 + assert ex.validate_required_startup_candles(600, '5m') == 2 + assert ex.validate_required_startup_candles(501, '5m') == 2 + assert ex.validate_required_startup_candles(499, '5m') == 1 + assert ex.validate_required_startup_candles(1000, '5m') == 3 + assert ex.validate_required_startup_candles(2499, '5m') == 5 + assert log_has_re(r'Using 5 calls to get OHLCV. This.*', caplog) - with pytest.raises(OperationalException, match=r'This strategy requires 600.*'): + with pytest.raises(OperationalException, match=r'This strategy requires 2500.*'): + ex.validate_required_startup_candles(2500, '5m') + + # Ensure the same also happens on init + default_conf['startup_candle_count'] = 6000 + with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'): Exchange(default_conf) + def test_exchange_has(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf) assert not exchange.exchange_has('ASDFASDF') @@ -1632,12 +1646,14 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: assert exchange._api_async.fetch_ohlcv.call_count == 2 exchange._api_async.fetch_ohlcv.reset_mock() + exchange.required_candle_call_count = 2 res = exchange.refresh_latest_ohlcv(pairs) assert len(res) == len(pairs) assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog) assert exchange._klines - assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert exchange._api_async.fetch_ohlcv.call_count == 4 + exchange._api_async.fetch_ohlcv.reset_mock() for pair in pairs: assert isinstance(exchange.klines(pair), DataFrame) assert len(exchange.klines(pair)) > 0 @@ -1653,7 +1669,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) assert len(res) == len(pairs) - assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert exchange._api_async.fetch_ohlcv.call_count == 0 assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, " f"timeframe {pairs[0][1]} ...", caplog) From de4bc7204d99c77afd19c446bc0811181911e616 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Nov 2021 13:14:29 +0100 Subject: [PATCH 22/76] Update documentation to clarify new behaviour --- docs/strategy-customization.md | 9 +++++++-- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 1 - 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 62e7509b3..d445234b5 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -134,7 +134,7 @@ Additional technical libraries can be installed as necessary, or custom indicato ### Strategy startup period -Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be. +Most indicators have an instable startup period, in which they are either not available (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be. To account for this, the strategy can be assigned the `startup_candle_count` attribute. This should be set to the maximum number of candles that the strategy requires to calculate stable indicators. @@ -146,8 +146,13 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100 By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt. +!!! Warning "Using x calls to get OHLCV" + If you receive a warning like `WARNING - Using 3 calls to get OHLCV. This can result in slower operations for the bot. Please check if you really need 1500 candles for your strategy` - you should consider if you really need this much lookback. + This will make Freqtrade take longer to refresh candles - and should be avoided if possible. + This is capped to 5 total calls to avoid overloading the exchange, or make freqtrade too slow. + !!! Warning - `startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations. + `startup_candle_count` should be below `ohlcv_candle_limit * 5` (which is 500 * 5 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations. #### Example diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4e9a6c71d..19ad4e4b6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -471,7 +471,7 @@ class Exchange: raise OperationalException( f'Time in force policies are not supported for {self.name} yet.') - def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None: + def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int: """ Checks if required startup_candles is more than ohlcv_candle_limit(). Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ff78321c5..8a8569dc4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -960,7 +960,6 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): Exchange(default_conf) - def test_exchange_has(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf) assert not exchange.exchange_has('ASDFASDF') From c11e1a84e4c231c09c765ff151e96c9708d7e75a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Nov 2021 15:41:04 +0100 Subject: [PATCH 23/76] Fix wrong logging detected in #5856 --- freqtrade/wallets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 237c1dc2c..79dfe2544 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -250,12 +250,13 @@ class Wallets: logger.warning("Minimum stake amount > available balance.") return 0 if min_stake_amount is not None and stake_amount < min_stake_amount: - stake_amount = min_stake_amount if self._log: logger.info( f"Stake amount for pair {pair} is too small " f"({stake_amount} < {min_stake_amount}), adjusting to {min_stake_amount}." ) + stake_amount = min_stake_amount + if stake_amount > max_stake_amount: stake_amount = max_stake_amount if self._log: From c54cf63baeaf8f30124c259579440f164edab738 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Nov 2021 03:01:06 +0000 Subject: [PATCH 24/76] Bump prompt-toolkit from 3.0.21 to 3.0.22 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.21 to 3.0.22. - [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.21...3.0.22) --- 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 90cfe46d5..aa467a009 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,6 +41,6 @@ psutil==5.8.0 colorama==0.4.4 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.21 +prompt-toolkit==3.0.22 # Extensions to datetime library python-dateutil==2.8.2 \ No newline at end of file From dbc863bcdf9a21d2958abcfe6c4b299986c22dee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Nov 2021 03:01:14 +0000 Subject: [PATCH 25/76] Bump jsonschema from 4.1.2 to 4.2.1 Bumps [jsonschema](https://github.com/Julian/jsonschema) from 4.1.2 to 4.2.1. - [Release notes](https://github.com/Julian/jsonschema/releases) - [Changelog](https://github.com/Julian/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/Julian/jsonschema/compare/v4.1.2...v4.2.1) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 90cfe46d5..231ec8f7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.2.1 cachetools==4.2.2 requests==2.26.0 urllib3==1.26.7 -jsonschema==4.1.2 +jsonschema==4.2.1 TA-Lib==0.4.21 technical==1.3.0 tabulate==0.8.9 From d0199b60144e0244ee62237752d8b36cece9e585 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Nov 2021 03:01:19 +0000 Subject: [PATCH 26/76] Bump coveralls from 3.2.0 to 3.3.0 Bumps [coveralls](https://github.com/TheKevJames/coveralls-python) from 3.2.0 to 3.3.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.2.0...3.3.0) --- updated-dependencies: - dependency-name: coveralls 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 d90ffcf78..c59fe4fe4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==3.2.0 +coveralls==3.3.0 flake8==4.0.1 flake8-tidy-imports==4.5.0 mypy==0.910 From a2c12f15f122861552fdca3fbad5a70dc3ffd4ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Nov 2021 03:01:25 +0000 Subject: [PATCH 27/76] Bump ccxt from 1.60.11 to 1.60.68 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.60.11 to 1.60.68. - [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.60.11...1.60.68) --- 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 90cfe46d5..d3e925082 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.3 pandas==1.3.4 pandas-ta==0.3.14b -ccxt==1.60.11 +ccxt==1.60.68 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 From ab06584a3e8df9b2f1a7ef25c97cc0372d86192f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Nov 2021 03:01:32 +0000 Subject: [PATCH 28/76] Bump numpy from 1.21.3 to 1.21.4 Bumps [numpy](https://github.com/numpy/numpy) from 1.21.3 to 1.21.4. - [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.21.3...v1.21.4) --- updated-dependencies: - dependency-name: numpy 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 90cfe46d5..096bd6ece 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.21.3 +numpy==1.21.4 pandas==1.3.4 pandas-ta==0.3.14b From b39794f8d234c1dc08a21de590fc136efb200453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Nov 2021 03:01:38 +0000 Subject: [PATCH 29/76] Bump scipy from 1.7.1 to 1.7.2 Bumps [scipy](https://github.com/scipy/scipy) from 1.7.1 to 1.7.2. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.7.1...v1.7.2) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 29616d4b2..7efbb47cd 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.7.1 +scipy==1.7.2 scikit-learn==1.0.1 scikit-optimize==0.9.0 filelock==3.3.2 From 3ce898e4a996b676e21abf854080ad8e9b68c811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Nov 2021 05:29:09 +0000 Subject: [PATCH 30/76] Bump isort from 5.9.3 to 5.10.0 Bumps [isort](https://github.com/pycqa/isort) from 5.9.3 to 5.10.0. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.9.3...5.10.0) --- updated-dependencies: - dependency-name: isort 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 c59fe4fe4..a66815daa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,7 +12,7 @@ pytest-asyncio==0.16.0 pytest-cov==3.0.0 pytest-mock==3.6.1 pytest-random-order==1.0.4 -isort==5.9.3 +isort==5.10.0 # For datetime mocking time-machine==2.4.0 From 84261237a0cb90051e493c9e0ceef8c7e37370f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Nov 2021 08:09:33 +0100 Subject: [PATCH 31/76] Improve doc wording --- docs/strategy-customization.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index d445234b5..f3658dd5a 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -147,8 +147,9 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100 By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt. !!! Warning "Using x calls to get OHLCV" - If you receive a warning like `WARNING - Using 3 calls to get OHLCV. This can result in slower operations for the bot. Please check if you really need 1500 candles for your strategy` - you should consider if you really need this much lookback. - This will make Freqtrade take longer to refresh candles - and should be avoided if possible. + If you receive a warning like `WARNING - Using 3 calls to get OHLCV. This can result in slower operations for the bot. Please check if you really need 1500 candles for your strategy` - you should consider if you really need this much historic data for your signals. + Having this will cause Freqtrade to make multiple calls for the same pair, which will obviously be slower than one network request. + As a consequence, Freqtrade will take longer to refresh candles - and should therefore be avoided if possible. This is capped to 5 total calls to avoid overloading the exchange, or make freqtrade too slow. !!! Warning From 63f4221f70575d19894fe7acea2d0fead86bb32e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Nov 2021 11:29:10 +0100 Subject: [PATCH 32/76] Fix broken documentation link --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 833c49812..292955346 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,7 +36,7 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange. -- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist)) +- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist)) - [X] [Bittrex](https://bittrex.com/) - [X] [FTX](https://ftx.com) - [X] [Gate.io](https://www.gate.io/ref/6266643) From e4cca631638e0380fbfd23a88387d46d6038800d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Nov 2021 06:44:55 +0100 Subject: [PATCH 33/76] Align sell_reason assignment location trade mode sets it after "exit confirmation" - so should backtesting detected in #5828 --- freqtrade/optimize/backtesting.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2f074500f..014ec8afc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -362,16 +362,6 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time - trade.sell_reason = sell.sell_reason - - # Checks and adds an exit tag, after checking that the length of the - # sell_row has the length for an exit tag column - if( - len(sell_row) > EXIT_TAG_IDX - and sell_row[EXIT_TAG_IDX] is not None - and len(sell_row[EXIT_TAG_IDX]) > 0 - ): - trade.sell_reason = sell_row[EXIT_TAG_IDX] 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) @@ -386,6 +376,17 @@ class Backtesting: current_time=sell_candle_time): return None + trade.sell_reason = sell.sell_reason + + # Checks and adds an exit tag, after checking that the length of the + # sell_row has the length for an exit tag column + if( + len(sell_row) > EXIT_TAG_IDX + and sell_row[EXIT_TAG_IDX] is not None + and len(sell_row[EXIT_TAG_IDX]) > 0 + ): + trade.sell_reason = sell_row[EXIT_TAG_IDX] + trade.close(closerate, show_msg=False) return trade From ae0e72a945ad2a3a69507ad289d0a923f122af93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Nov 2021 19:59:29 +0100 Subject: [PATCH 34/76] Provide strategy with copied objects avoids accidental modification of crucial elements in a trade object part of #5828 --- freqtrade/strategy/strategy_wrapper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/strategy/strategy_wrapper.py b/freqtrade/strategy/strategy_wrapper.py index 121189b68..9aead8395 100644 --- a/freqtrade/strategy/strategy_wrapper.py +++ b/freqtrade/strategy/strategy_wrapper.py @@ -1,4 +1,5 @@ import logging +from copy import deepcopy from freqtrade.exceptions import StrategyError @@ -14,6 +15,9 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_err """ def wrapper(*args, **kwargs): try: + if 'trade' in kwargs: + # Protect accidental modifications from within the strategy + kwargs['trade'] = deepcopy(kwargs['trade']) return f(*args, **kwargs) except ValueError as error: logger.warning( From 2bfec7d549dd62ab4319b130e94a3707039f7749 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Nov 2021 20:14:32 +0100 Subject: [PATCH 35/76] Add small test-case confirming trade object copy --- tests/test_freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 47b76af61..a417a46eb 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2579,6 +2579,8 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 + assert id(freqtrade.strategy.confirm_trade_exit.call_args_list[0][1]['trade']) != id(trade) + assert freqtrade.strategy.confirm_trade_exit.call_args_list[0][1]['trade'].id == trade.id # Repatch with true freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) From f9e5a25b36c143765da265e7d7e33e46bd9f3322 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Nov 2021 07:48:25 +0000 Subject: [PATCH 36/76] Add docstring style to Contributing --- CONTRIBUTING.md | 7 +++++++ docs/developer.md | 2 ++ freqtrade/persistence/models.py | 2 ++ 3 files changed, 11 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4ccc1b9f..b4e0bc024 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,13 @@ To help with that, we encourage you to install the git pre-commit hook that will warn you when you try to commit code that fails these checks. Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html). +##### Additional styles applied + +* Have docstrings on all public methods +* Use double-quotes for docstrings +* Multiline docstrings should be indented to the level of the first quote +* Doc-strings should follow the reST format (`:param xxx: ...`, `:return: ...`, `:raises KeyError: ... `) + ### 3. Test if all type-hints are correct #### Run mypy diff --git a/docs/developer.md b/docs/developer.md index 01f274131..b69a70aa3 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -26,6 +26,8 @@ Alternatively (e.g. if your system is not supported by the setup.sh script), fol This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. +Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md). + ### Devcontainer setup The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension. diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 61e59b7c3..df3b71acb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -195,6 +195,8 @@ class Order(_DECL_BASE): @staticmethod def get_open_orders() -> List['Order']: """ + Retrieve open orders from the database + :return: List of open orders """ return Order.query.filter(Order.ft_is_open.is_(True)).all() From 6267678ca9e77c46bda8cccec60baec9f891db64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Nov 2021 10:31:54 +0000 Subject: [PATCH 37/76] Use doublequotes for docstrings --- freqtrade/exchange/kucoin.py | 8 ++++---- freqtrade/exchange/okex.py | 6 +++--- tests/exchange/test_exchange.py | 8 +++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 5d818f6a2..2884669a6 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -1,4 +1,4 @@ -""" Kucoin exchange subclass """ +"""Kucoin exchange subclass.""" import logging from typing import Dict @@ -9,9 +9,9 @@ logger = logging.getLogger(__name__) class Kucoin(Exchange): - """ - Kucoin exchange class. Contains adjustments needed for Freqtrade to work - with this 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 diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index 5ab6f3147..ec31be3a3 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -8,9 +8,9 @@ logger = logging.getLogger(__name__) class Okex(Exchange): - """ - Okex exchange class. Contains adjustments needed for Freqtrade to work - with this exchange. + """Okex exchange class. + + Contains adjustments needed for Freqtrade to work with this exchange. """ _ft_has: Dict = { diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8a8569dc4..ced7d8d36 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -238,9 +238,9 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog): (2.9999, 4, 0.005, 2.995), ]) def test_amount_to_precision(default_conf, mocker, amount, precision_mode, precision, expected): - ''' + """ Test rounds down - ''' + """ markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}}) @@ -279,9 +279,7 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci ]) def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected): - ''' - Test price to precision - ''' + """Test price to precision""" markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}}) exchange = get_patched_exchange(mocker, default_conf, id="binance") From 6f0a98229feeda612e5d2e157fbc499252fda738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20P=C3=A9rez=20Alarc=C3=B3n?= Date: Tue, 9 Nov 2021 12:27:38 +0000 Subject: [PATCH 38/76] docs: removes duplicated "without" in pairlists.md --- 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 589bc23b2..ec9c92104 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -198,7 +198,7 @@ Not defining this parameter (or setting it to 0) will use all-time performance. The optional `min_profit` parameter defines the minimum profit a pair must have to be considered. Pairs below this level will be filtered out. -Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without without a way to recover. +Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without a way to recover. ```json "pairlists": [ From e8b4d44881988ba4190ff7fd23630a8b3273f8c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Nov 2021 15:16:51 +0100 Subject: [PATCH 39/76] Add warning about telegram group usage --- docs/telegram-usage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 0c1af9f4d..c7f9c58f6 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -58,6 +58,8 @@ For the Freqtrade configuration, you can then use the the full value (including ```json "chat_id": "-1001332619709" ``` +!!! Warning "Using telegram groups" + When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasent surprises. ## Control telegram noise From c9d974d2108ae959f4cce7c0f60f27ca334a8286 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Nov 2021 19:52:05 +0100 Subject: [PATCH 40/76] Clarify performancefilter docs closes #5870 --- docs/includes/pairlists.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index ec9c92104..d52b685c1 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -211,6 +211,8 @@ Using this parameter without `minutes` is highly discouraged, as it can lead to ], ``` +As this Filter uses past performance of the bot, it'll have some startup-period - and should only be used after the bot has a few 100 trades in the database. + !!! Warning "Backtesting" `PerformanceFilter` does not support backtesting mode. From 23a566b478ab020910903c525c335d8cdac02f15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Nov 2021 06:38:24 +0100 Subject: [PATCH 41/76] validate_stake_amount should not be a private method --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/wallets.py | 4 ++-- tests/test_wallets.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 11d79b775..4e004a6ab 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -501,7 +501,7 @@ class FreqtradeBot(LoggingMixin): pair=pair, current_time=datetime.now(timezone.utc), current_rate=enter_limit_requested, proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) - stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) + stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 014ec8afc..49957c2bb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -431,7 +431,7 @@ class Backtesting: default_retval=stake_amount)( pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) - stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) + stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: return None diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 79dfe2544..4357f2a95 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -238,7 +238,7 @@ class Wallets: return self._check_available_stake_amount(stake_amount, available_amount) - def _validate_stake_amount(self, pair, stake_amount, min_stake_amount): + def validate_stake_amount(self, pair, stake_amount, min_stake_amount): if not stake_amount: logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.") return 0 @@ -258,10 +258,10 @@ class Wallets: stake_amount = min_stake_amount if stake_amount > max_stake_amount: - stake_amount = max_stake_amount if self._log: logger.info( f"Stake amount for pair {pair} is too big " f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}." ) + stake_amount = max_stake_amount return stake_amount diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 53e3b758e..c223a9dd1 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -189,13 +189,13 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r (1, 15, 10, 0), # Below min stake and min_stake > max_stake ]) -def test__validate_stake_amount(mocker, default_conf, - stake_amount, min_stake_amount, max_stake_amount, expected): +def test_validate_stake_amount(mocker, default_conf, + stake_amount, min_stake_amount, max_stake_amount, expected): freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch("freqtrade.wallets.Wallets.get_available_stake_amount", return_value=max_stake_amount) - res = freqtrade.wallets._validate_stake_amount('XRP/USDT', stake_amount, min_stake_amount) + res = freqtrade.wallets.validate_stake_amount('XRP/USDT', stake_amount, min_stake_amount) assert res == expected From d3d17f9f8b19b1ff87bcb4cb705d49363593c49c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Nov 2021 06:57:22 +0100 Subject: [PATCH 42/76] Only allow min-stake adjustments of up to 30% fix #5856 --- docs/configuration.md | 2 +- freqtrade/wallets.py | 9 +++++++++ tests/test_freqtradebot.py | 2 +- tests/test_wallets.py | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f2958f725..6c810fba2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -214,7 +214,7 @@ With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05) 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. + 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. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected. #### Tradable balance diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4357f2a95..1a124d1bb 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -255,6 +255,15 @@ class Wallets: f"Stake amount for pair {pair} is too small " f"({stake_amount} < {min_stake_amount}), adjusting to {min_stake_amount}." ) + if stake_amount * 1.3 < min_stake_amount: + # Top-cap stake-amount adjustments to +30%. + if self._log: + logger.info( + f"Adjusted stake amount for pair {pair} is more than 30% bigger than " + f"the desired stake ({stake_amount} * 1.3 > {max_stake_amount}), " + f"ignoring trade." + ) + return 0 stake_amount = min_stake_amount if stake_amount > max_stake_amount: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a417a46eb..0b3c76736 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -321,7 +321,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke @pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ (5.0, True, True, 99), - (0.00005, True, False, 99), + (0.04, True, False, 99), # Amount will be adjusted to min - which is 0.051 (0, False, True, 99), (UNLIMITED_STAKE_AMOUNT, False, True, 0), ]) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index c223a9dd1..3e02cdb09 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -185,8 +185,9 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r (100, 11, 500, 100), (1000, 11, 500, 500), # Above max-stake (20, 15, 10, 0), # Minimum stake > max-stake - (1, 11, 100, 11), # Below min stake + (9, 11, 100, 11), # Below min stake (1, 15, 10, 0), # Below min stake and min_stake > max_stake + (20, 50, 100, 0), # Below min stake and stake * 1.3 > min_stake ]) def test_validate_stake_amount(mocker, default_conf, From e7d1630c9272e1a247d71360cebde088cb6be3ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Nov 2021 16:51:31 +0100 Subject: [PATCH 43/76] Add space --- tests/test_freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0b3c76736..f4d102113 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -321,7 +321,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke @pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ (5.0, True, True, 99), - (0.04, True, False, 99), # Amount will be adjusted to min - which is 0.051 + (0.04, True, False, 99), # Amount will be adjusted to min - which is 0.051 (0, False, True, 99), (UNLIMITED_STAKE_AMOUNT, False, True, 0), ]) From f7b2c0c5d7f8785dd73896f1b8293e5a04733e87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Nov 2021 19:16:37 +0100 Subject: [PATCH 44/76] Remove unneeded assignment from tests --- tests/test_freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f4d102113..0e49590a4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1618,7 +1618,6 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_buy_order_usd trade = MagicMock() trade.open_order_id = None - trade.open_fee = 0.001 trade.pair = 'ETH/USDT' trades = [trade] @@ -1725,7 +1724,6 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, trade = MagicMock() trade.open_order_id = '123' - trade.open_fee = 0.001 # Test raise of OperationalException exception mocker.patch( @@ -1743,7 +1741,6 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) -> trade = MagicMock() trade.open_order_id = '123' - trade.open_fee = 0.001 # Test raise of OperationalException exception grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock()) From f8d30abd79fec7948e4ce7376e144c2f97bea739 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Nov 2021 19:43:36 +0100 Subject: [PATCH 45/76] Handle order returns that contain trades directly binance market orders - and potentially other exchanges --- freqtrade/freqtradebot.py | 13 ++++++++----- tests/conftest.py | 40 ++++++++++++++++++++++++++++++++++++++ tests/test_freqtradebot.py | 25 ++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4e004a6ab..db0453cd7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1381,14 +1381,17 @@ class FreqtradeBot(LoggingMixin): return self.apply_fee_conditional(trade, trade_base_currency, amount=order_amount, fee_abs=fee_cost) return order_amount - return self.fee_detection_from_trades(trade, order, order_amount) + return self.fee_detection_from_trades(trade, order, order_amount, order.get('trades', [])) - def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: + def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float, + trades: List) -> float: """ - fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. + fee-detection fallback to Trades. + Either uses provided trades list or the result of fetch_my_trades to get correct fee. """ - trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order), - trade.pair, trade.open_date) + if not trades: + 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/conftest.py b/tests/conftest.py index 698c464ed..751cb5f9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2221,6 +2221,46 @@ def market_buy_order_usdt(): } +@pytest.fixture +def market_buy_order_usdt_doublefee(market_buy_order_usdt): + order = deepcopy(market_buy_order_usdt) + order['fee'] = None + # Market orders filled with 2 trades can have fees in different currencies + # assuming the account runs out of BNB. + order['fees'] = [ + {'cost': 0.00025125, 'currency': 'BNB'}, + {'cost': 0.05030681, 'currency': 'USDT'}, + ] + order['trades'] = [{ + 'timestamp': None, + 'datetime': None, + 'symbol': 'ETH/USDT', + 'id': None, + 'order': '123', + 'type': 'market', + 'side': 'sell', + 'takerOrMaker': None, + 'price': 2.01, + 'amount': 25.0, + 'cost': 50.25, + 'fee': {'cost': 0.00025125, 'currency': 'BNB'} + }, { + 'timestamp': None, + 'datetime': None, + 'symbol': 'ETH/USDT', + 'id': None, + 'order': '123', + 'type': 'market', + 'side': 'sell', + 'takerOrMaker': None, + 'price': 2.0, + 'amount': 5, + 'cost': 10, + 'fee': {'cost': 0.0100306, 'currency': 'USDT'} + }] + return order + + @pytest.fixture def market_sell_order_usdt(): return { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0e49590a4..1f9b1d6b3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3634,6 +3634,31 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_ assert freqtrade.get_real_amount(trade, limit_buy_order_usdt) == amount +def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee, + fee, mocker): + + tfo_mock = mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', return_value='BNB/USDT') + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 200}) + trade = Trade( + pair='LTC/USDT', + amount=30.0, + exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.245441, + open_order_id="123456" + ) + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + + # Amount does not change + assert trade.fee_open == 0.0025 + assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee) == 30.0 + assert tfo_mock.call_count == 0 + # Fetch fees from trades dict if available to get "proper" values + assert round(trade.fee_open, 4) == 0.001 + + def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_order_fee, fee, mocker): limit_buy_order_usdt = deepcopy(buy_order_fee) From ce2aa1dc698f30a84345f1b3a2bd6cf7cd928056 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Nov 2021 12:06:18 +0100 Subject: [PATCH 46/76] Small formatting upgrades --- freqtrade/exceptions.py | 2 -- freqtrade/strategy/hyper.py | 2 +- freqtrade/strategy/interface.py | 2 +- freqtrade/wallets.py | 2 +- scripts/rest_client.py | 8 ++++---- tests/test_plotting.py | 1 - 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 056be8720..6b0039a3f 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -1,5 +1,3 @@ - - class FreqtradeException(Exception): """ Freqtrade base exception. Handled at the outermost level. diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index eaf41263a..278954bb2 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -292,7 +292,7 @@ class BooleanParameter(CategoricalParameter): load=load, **kwargs) -class HyperStrategyMixin(object): +class HyperStrategyMixin: """ A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell strategy logic. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d11097ed2..47a226f16 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -30,7 +30,7 @@ logger = logging.getLogger(__name__) CUSTOM_SELL_MAX_LENGTH = 64 -class SellCheckTuple(object): +class SellCheckTuple: """ NamedTuple for Sell type + reason """ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 1a124d1bb..d10847099 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -73,7 +73,7 @@ class Wallets: tot_profit = Trade.get_total_closed_profit() else: tot_profit = LocalTrade.total_profit - tot_in_trades = sum([trade.stake_amount for trade in open_trades]) + tot_in_trades = sum(trade.stake_amount for trade in open_trades) current_stake = self.start_cap + tot_profit - tot_in_trades _wallets[self._config['stake_currency']] = Wallet( diff --git a/scripts/rest_client.py b/scripts/rest_client.py index ccb34d81f..b1234d329 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -39,7 +39,7 @@ class FtRestClient(): def _call(self, method, apipath, params: dict = None, data=None, files=None): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): - raise ValueError('invalid method <{0}>'.format(method)) + raise ValueError(f'invalid method <{method}>') basepath = f"{self._serverurl}/api/v1/{apipath}" hd = {"Accept": "application/json", @@ -124,7 +124,7 @@ class FtRestClient(): :param lock_id: ID for the lock to delete :return: json object """ - return self._delete("locks/{}".format(lock_id)) + return self._delete(f"locks/{lock_id}") def daily(self, days=None): """Return the profits for each day, and amount of trades. @@ -220,7 +220,7 @@ class FtRestClient(): :param trade_id: Specify which trade to get. :return: json object """ - return self._get("trade/{}".format(trade_id)) + return self._get(f"trade/{trade_id}") def delete_trade(self, trade_id): """Delete trade from the database. @@ -229,7 +229,7 @@ class FtRestClient(): :param trade_id: Deletes the trade with this ID from the database. :return: json object """ - return self._delete("trades/{}".format(trade_id)) + return self._delete(f"trades/{trade_id}") def whitelist(self): """Show the current whitelist. diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 51301a464..8cbe7f863 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1,4 +1,3 @@ - from copy import deepcopy from pathlib import Path from unittest.mock import MagicMock From 1b271d08404b59f4ab4d30b0d44a4cacecf6e7ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Nov 2021 12:58:38 +0100 Subject: [PATCH 47/76] Improve % outputs to not use explicit "pct" entries --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 3 +- freqtrade/rpc/telegram.py | 38 ++++++++++++------------- tests/rpc/test_rpc_apiserver.py | 1 + tests/rpc/test_rpc_telegram.py | 10 +++---- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 5dbd1f637..c9ff0ddaf 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -95,6 +95,7 @@ class Profit(BaseModel): avg_duration: str best_pair: str best_rate: float + best_pair_profit_ratio: float winning_trades: int losing_trades: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 97a0def0b..db021029b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -534,7 +534,8 @@ class RPC: 'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0, 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_pair': best_pair[0] if best_pair else '', - 'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, + 'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated + 'best_pair_profit_ratio': best_pair[1] if best_pair else 0, 'winning_trades': winning_trades, 'losing_trades': losing_trades, } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1dea5fae7..f53154dff 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -264,7 +264,7 @@ class Telegram(RPCHandler): msg['profit_extra'] = '' message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" + "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" "*Buy Tag:* `{buy_tag}`\n" "*Sell Reason:* `{sell_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" @@ -397,19 +397,19 @@ class Telegram(RPCHandler): "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") - + "`{profit_pct:.2f}%`", + + "`{profit_ratio:.2%}`", ] if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] - and r['initial_stop_loss_pct'] is not None): + and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` " - "`({initial_stop_loss_pct:.2f}%)`") + "`({initial_stop_loss_ratio:.2%})`") # Adding stoploss and stoploss percentage only if it is not None lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " + - ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else "")) + ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else "")) lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " - "`({stoploss_current_dist_pct:.2f}%)`") + "`({stoploss_current_dist_ratio:.2%})`") if r['open_order']: if r['sell_order_status']: lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") @@ -612,11 +612,11 @@ class Telegram(RPCHandler): fiat_disp_cur, start_date) profit_closed_coin = stats['profit_closed_coin'] - profit_closed_percent_mean = stats['profit_closed_percent_mean'] + profit_closed_ratio_mean = stats['profit_closed_ratio_mean'] profit_closed_percent = stats['profit_closed_percent'] profit_closed_fiat = stats['profit_closed_fiat'] profit_all_coin = stats['profit_all_coin'] - profit_all_percent_mean = stats['profit_all_percent_mean'] + profit_all_ratio_mean = stats['profit_all_ratio_mean'] profit_all_percent = stats['profit_all_percent'] profit_all_fiat = stats['profit_all_fiat'] trade_count = stats['trade_count'] @@ -624,7 +624,7 @@ class Telegram(RPCHandler): latest_trade_date = stats['latest_trade_date'] avg_duration = stats['avg_duration'] best_pair = stats['best_pair'] - best_rate = stats['best_rate'] + best_pair_profit_ratio = stats['best_pair_profit_ratio'] if stats['trade_count'] == 0: markdown_msg = 'No trades yet.' else: @@ -632,7 +632,7 @@ class Telegram(RPCHandler): if stats['closed_trade_count'] > 0: markdown_msg = ("*ROI:* Closed trades\n" f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} " - f"({profit_closed_percent_mean:.2f}%) " + f"({profit_closed_ratio_mean:.2%}) " f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n") else: @@ -641,7 +641,7 @@ class Telegram(RPCHandler): 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_ratio_mean:.2%}) " f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" f"*Total Trade Count:* `{trade_count}`\n" @@ -652,7 +652,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}%`") + f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`") self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", query=update.callback_query) @@ -755,10 +755,10 @@ class Telegram(RPCHandler): output += ("\n*Estimated Value*:\n" f"\t`{result['stake']}: " f"{round_coin_value(result['total'], result['stake'], False)}`" - f" `({result['starting_capital_pct']}%)`\n" + f" `({result['starting_capital_ratio']:.2%})`\n" f"\t`{result['symbol']}: " f"{round_coin_value(result['value'], result['symbol'], False)}`" - f" `({result['starting_capital_fiat_pct']}%)`\n") + f" `({result['starting_capital_fiat_ratio']:.2%})`\n") self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: @@ -893,7 +893,7 @@ class Telegram(RPCHandler): trades_tab = tabulate( [[arrow.get(trade['close_date']).humanize(), trade['pair'] + " (#" + str(trade['trade_id']) + ")", - f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] + f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})"] for trade in trades['trades']], headers=[ 'Close Date', @@ -945,7 +945,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i+1}.\t {trade['pair']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit_pct']:.2f}%) " + f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -980,7 +980,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i+1}.\t {trade['buy_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit_pct']:.2f}%) " + f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -1015,7 +1015,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i+1}.\t {trade['sell_reason']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit_pct']:.2f}%) " + f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -1050,7 +1050,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i+1}.\t {trade['mix_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit']:.2f}%) " + f"({trade['profit']:.2%}) " f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3f908377b..e650e5142 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -717,6 +717,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets): assert rc.json() == {'avg_duration': ANY, 'best_pair': 'XRP/BTC', 'best_rate': 1.0, + 'best_pair_profit_ratio': 0.01, 'first_trade_date': ANY, 'first_trade_timestamp': ANY, 'latest_trade_date': '5 minutes ago', diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 74c33d2af..68f7457d5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -189,16 +189,16 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'amount': 90.99181074, 'stake_amount': 90.99181074, 'buy_tag': None, - 'close_profit_pct': None, + 'close_profit_ratio': None, 'profit': -0.0059, - 'profit_pct': -0.59, + 'profit_ratio': -0.0059, 'initial_stop_loss_abs': 1.098e-05, 'stop_loss_abs': 1.099e-05, 'sell_order_status': None, - 'initial_stop_loss_pct': -0.05, + 'initial_stop_loss_ratio': -0.0005, 'stoploss_current_dist': 1e-08, - 'stoploss_current_dist_pct': -0.02, - 'stop_loss_pct': -0.01, + 'stoploss_current_dist_ratio': -0.0002, + 'stop_loss_ratio': -0.0001, 'open_order': '(limit buy rem=0.00000000)', 'is_open': True }]), From 4eb903835839dfb1d52a218415d447f6f90f4f80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Nov 2021 13:55:55 +0100 Subject: [PATCH 48/76] Some more fixes to % formatting --- freqtrade/optimize/hyperopt_tools.py | 8 ++++---- freqtrade/optimize/optimize_reports.py | 10 +++++----- freqtrade/persistence/models.py | 3 ++- freqtrade/plot/plotting.py | 4 ++-- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- freqtrade/plugins/pairlist/SpreadFilter.py | 2 +- freqtrade/rpc/rpc.py | 3 +-- freqtrade/strategy/interface.py | 2 +- tests/rpc/test_rpc.py | 10 +++++----- tests/test_freqtradebot.py | 4 ++-- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 0b2efa5c2..1204320da 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -284,10 +284,10 @@ class HyperoptTools(): 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'] * 100: 7.2f}%). " + f"Avg profit {results_metrics['profit_mean']:7.2%}. " + f"Median profit {results_metrics['profit_median']:7.2%}. " + f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} " + f"({results_metrics['profit_total']:8.2%}). " f"Avg duration {results_metrics['holding_avg']} min." ) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 14d928e54..417bb54ae 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -725,7 +725,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"{strat_results['profit_total']:.2%}"), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', f"{round(strat_results['profit_total'] / strat_results['backtest_days'] * 100, 2)}%"), @@ -738,9 +738,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), ('Worst Pair', f"{strat_results['worst_pair']['key']} " f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), - ('Best trade', f"{best_trade['pair']} {round(best_trade['profit_ratio'] * 100, 2)}%"), + ('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"), ('Worst trade', f"{worst_trade['pair']} " - f"{round(worst_trade['profit_ratio'] * 100, 2)}%"), + f"{worst_trade['profit_ratio']:.2%}"), ('Best day', round_coin_value(strat_results['backtest_best_day_abs'], strat_results['stake_currency'])), @@ -758,7 +758,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Max balance', round_coin_value(strat_results['csum_max'], strat_results['stake_currency'])), - ('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), + ('Drawdown', f"{strat_results['max_drawdown']:.2%}"), ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], strat_results['stake_currency'])), ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], @@ -767,7 +767,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Drawdown Start', strat_results['drawdown_start']), ('Drawdown End', strat_results['drawdown_end']), - ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), + ('Market change', f"{strat_results['market_change']:.2%}"), ] return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index df3b71acb..2fcdd58bb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -972,6 +972,7 @@ class Trade(_DECL_BASE, LocalTrade): if not any(item["mix_tag"] == mix_tag for item in return_list): return_list.append({'mix_tag': mix_tag, 'profit': profit, + 'profit_pct': round(profit * 100, 2), 'profit_abs': profit_abs, 'count': count}) else: @@ -980,11 +981,11 @@ class Trade(_DECL_BASE, LocalTrade): return_list[i] = { 'mix_tag': mix_tag, 'profit': profit + return_list[i]["profit"], + 'profit_pct': round(profit + return_list[i]["profit"] * 100, 2), 'profit_abs': profit_abs + return_list[i]["profit_abs"], 'count': 1 + return_list[i]["count"]} i += 1 - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in return_list] return return_list @staticmethod diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 509c03e90..d2b51f3a2 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -169,8 +169,8 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'], ], mode='markers', - name=f"Max drawdown {max_drawdown * 100:.2f}%", - text=f"Max drawdown {max_drawdown * 100:.2f}%", + name=f"Max drawdown {max_drawdown:.2%}", + text=f"Max drawdown {max_drawdown:.2%}", marker=dict( symbol='square-open', size=9, diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index 5b5afb557..7704ea9aa 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -82,7 +82,7 @@ class PriceFilter(IPairList): changeperc = compare / ticker['last'] if changeperc > self._low_price_ratio: self.log_once(f"Removed {pair} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%", logger.info) + f"because 1 unit is {changeperc:.3%}", logger.info) return False # Perform low_amount check diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 1b152774b..20cc97c6a 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -47,7 +47,7 @@ class SpreadFilter(IPairList): spread = 1 - ticker['bid'] / ticker['ask'] if spread > self._max_spread_ratio: self.log_once(f"Removed {pair} from whitelist, because spread " - f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%", + f"{spread * 100:.3%} > {self._max_spread_ratio:.3%}", logger.info) return False else: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index db021029b..de066d123 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -224,9 +224,8 @@ class RPC: trade.pair, refresh=False, side="sell") except (PricingError, ExchangeError): current_rate = NAN - trade_percent = (100 * trade.calc_profit_ratio(current_rate)) trade_profit = trade.calc_profit(current_rate) - profit_str = f'{trade_percent:.2f}%' + profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' if self._fiat_converter: fiat_profit = self._fiat_converter.convert_amount( trade_profit, diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 47a226f16..d4b496ed0 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -765,7 +765,7 @@ class IStrategy(ABC, HyperStrategyMixin): if self.trailing_stop_positive is not None and high_profit > sl_offset: stop_loss_value = self.trailing_stop_positive logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} " - f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") + f"offset: {sl_offset:.4g} profit: {current_profit:.2%}") trade.adjust_stop_loss(high or current_rate, stop_loss_value) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 945217b8a..2852ada81 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1004,7 +1004,7 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, assert len(res) == 1 assert res[0]['mix_tag'] == 'Other Other' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + assert prec_satoshi(res[0]['profit_pct'], 6.2) trade.buy_tag = "TESTBUY" trade.sell_reason = "TESTSELL" @@ -1013,7 +1013,7 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, assert len(res) == 1 assert res[0]['mix_tag'] == 'TESTBUY TESTSELL' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + assert prec_satoshi(res[0]['profit_pct'], 6.2) def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): @@ -1032,10 +1032,10 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['mix_tag'] == 'TEST1 sell_signal' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 0.5) + assert prec_satoshi(res[0]['profit_pct'], 0.5) assert res[1]['mix_tag'] == 'Other roi' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit'], 1.0) + assert prec_satoshi(res[1]['profit_pct'], 1.0) # Test for a specific pair res = rpc._rpc_mix_tag_performance('ETC/BTC') @@ -1043,7 +1043,7 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['mix_tag'] == 'TEST1 sell_signal' - assert prec_satoshi(res[0]['profit'], 0.5) + assert prec_satoshi(res[0]['profit_pct'], 0.5) def test_rpc_count(mocker, default_conf, ticker, fee) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1f9b1d6b3..629fed76b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3352,7 +3352,7 @@ def test_trailing_stop_loss_positive( ) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - caplog_text = f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: 0.0249%" + caplog_text = f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: 2.49%" if trail_if_reached: assert not log_has(caplog_text, caplog) assert not log_has("ETH/USDT - Adjusting stoploss...", caplog) @@ -3372,7 +3372,7 @@ def test_trailing_stop_loss_positive( ) assert freqtrade.handle_trade(trade) is False assert log_has( - f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: 0.0572%", + f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: 5.72%", caplog ) assert log_has("ETH/USDT - Adjusting stoploss...", caplog) From e0fd880c11572ff83aa444008c33805964426a41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Nov 2021 15:58:30 +0100 Subject: [PATCH 49/76] Improve some more pct formattings --- freqtrade/data/converter.py | 2 +- freqtrade/optimize/optimize_reports.py | 8 ++++---- freqtrade/plot/plotting.py | 2 +- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- freqtrade/plugins/pairlist/SpreadFilter.py | 2 +- tests/test_plotting.py | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index ca6464965..d592b4990 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" - {pct_missing:.2%}") if pct_missing > 0.01: logger.info(message) else: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 417bb54ae..c4002fcbe 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -728,16 +728,16 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Total profit %', f"{strat_results['profit_total']:.2%}"), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', - f"{round(strat_results['profit_total'] / strat_results['backtest_days'] * 100, 2)}%"), + f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), ('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'], strat_results['stake_currency'])), ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " - f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), + f"{strat_results['best_pair']['profit_sum']:.2%}"), ('Worst Pair', f"{strat_results['worst_pair']['key']} " - f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), + f"{strat_results['worst_pair']['profit_sum']:.2%}"), ('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"), ('Worst trade', f"{worst_trade['pair']} " f"{worst_trade['profit_ratio']:.2%}"), @@ -864,5 +864,5 @@ def show_sorted_pairlist(config: Dict, backtest_stats: Dict): print(f"Pairs for Strategy {strategy}: \n[") for result in results['results_per_pair']: if result["key"] != 'TOTAL': - print(f'"{result["key"]}", // {round(result["profit_mean_pct"], 2)}%') + print(f'"{result["key"]}", // {result["profit_mean"]:.2%}') print("]") diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index d2b51f3a2..6d44d56b1 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -192,7 +192,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: # Trades can be empty if trades is not None and len(trades) > 0: # Create description for sell summarizing the trade - trades['desc'] = trades.apply(lambda row: f"{round(row['profit_ratio'] * 100, 1)}%, " + trades['desc'] = trades.apply(lambda row: f"{row['profit_ratio']:.2%}, " f"{row['sell_reason']}, " f"{row['trade_duration']} min", axis=1) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index 7704ea9aa..63623d8c8 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -50,7 +50,7 @@ class PriceFilter(IPairList): """ active_price_filters = [] if self._low_price_ratio != 0: - active_price_filters.append(f"below {self._low_price_ratio * 100}%") + active_price_filters.append(f"below {self._low_price_ratio:.1%}") if self._min_price != 0: active_price_filters.append(f"below {self._min_price:.8f}") if self._max_price != 0: diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 20cc97c6a..2d6e728ec 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -34,7 +34,7 @@ class SpreadFilter(IPairList): Short whitelist method description - used for startup-messages """ return (f"{self.name} - Filtering pairs with ask/bid diff above " - f"{self._max_spread_ratio * 100}%.") + f"{self._max_spread_ratio:.2%}.") def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 8cbe7f863..8a40f4a20 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -171,7 +171,7 @@ def test_plot_trades(testdatadir, caplog): assert len(trades) == len(trade_buy.x) assert trade_buy.marker.color == 'cyan' assert trade_buy.marker.symbol == 'circle-open' - assert trade_buy.text[0] == '4.0%, roi, 15 min' + assert trade_buy.text[0] == '3.99%, roi, 15 min' trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit') assert isinstance(trade_sell, go.Scatter) @@ -179,7 +179,7 @@ def test_plot_trades(testdatadir, caplog): assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_sell.x) assert trade_sell.marker.color == 'green' assert trade_sell.marker.symbol == 'square-open' - assert trade_sell.text[0] == '4.0%, roi, 15 min' + assert trade_sell.text[0] == '3.99%, roi, 15 min' trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss') assert isinstance(trade_sell_loss, go.Scatter) @@ -187,7 +187,7 @@ def test_plot_trades(testdatadir, caplog): assert len(trades.loc[trades['profit_ratio'] <= 0]) == len(trade_sell_loss.x) assert trade_sell_loss.marker.color == 'red' assert trade_sell_loss.marker.symbol == 'square-open' - assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min' + assert trade_sell_loss.text[5] == '-10.45%, stop_loss, 720 min' def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog): From 39bb34cdb3405db82961f886c560ea72c4a3dfbc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Nov 2021 16:34:40 +0100 Subject: [PATCH 50/76] Fix test loading bug --- tests/optimize/test_hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b123fec21..a43c62376 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -424,6 +424,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) patch_exchange(mocker) mocker.patch.object(Path, 'open') + mocker.patch('freqtrade.configuration.config_validation.validate_config_schema') mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) optimizer_param = { From a237667bc98b309dcd5d2f52d65beff2f3198949 Mon Sep 17 00:00:00 2001 From: ethan Date: Fri, 12 Nov 2021 16:18:04 -0500 Subject: [PATCH 51/76] Update buy/sell fill telegram notifications --- freqtrade/rpc/telegram.py | 105 +++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 23 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f53154dff..2a700348f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -28,7 +28,6 @@ from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException, RPCHandler - logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') @@ -227,7 +226,7 @@ class Telegram(RPCHandler): ) if msg.get('buy_tag', None): content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f}`\n") + content.append(f"*Amount:* `{msg['amount']:.8f} {msg['pair'].split('/')[0]}`\n") content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n") content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n") content.append( @@ -242,7 +241,66 @@ class Telegram(RPCHandler): message += ")`" return message + def _format_buy_msg_fill(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 + + content = [] + content.append( + f"\N{CHECK MARK} *{msg['exchange']}:* Bought {msg['pair']}" + f" (#{msg['trade_id']})\n" + ) + if msg.get('buy_tag', None): + content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") + content.append(f"*Amount:* `{msg['amount']:.8f} {msg['pair'].split('/')[0]}`\n") + content.append( + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" + ) + if msg.get('fiat_currency', None): + content.append( + f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + ) + + message = ''.join(content) + message += ")`" + return message + def _format_sell_msg(self, msg: Dict[str, Any]) -> str: + msg['amount'] = round(msg['amount'], 8) + msg['duration'] = msg['close_date'].replace( + microsecond=0) - msg['open_date'].replace(microsecond=0) + msg['duration_min'] = msg['duration'].total_seconds() / 60 + msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None + + # 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']) + msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}' + ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) + else: + msg['profit_extra'] = '' + + msg['currency'] = msg['pair'].split('/')[0] + message = ("\N{LARGE RED CIRCLE} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Unrealized Profit:* `{profit_ratio:.2%}{profit_extra}`\n" + "*Buy Tag:* `{buy_tag}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Amount:* `{amount:.8f}` {currency}\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{close_rate:.8f}`").format( + **msg) # TODO: Updated from `limit`, confirm this is correct variable? + + return message + + def _format_sell_msg_fill(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( @@ -263,15 +321,17 @@ class Telegram(RPCHandler): else: msg['profit_extra'] = '' - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + msg['currency'] = msg['pair'].split('/')[0] + import pprint + pprint.pprint(msg) + message = ("{emoji} *{exchange}:* Sold {pair} (#{trade_id})\n" "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" "*Buy Tag:* `{buy_tag}`\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) + "*Amount:* `{amount:.8f}` {currency}\n" + "*Close Rate:* `{close_rate:.8f}`").format( + **msg) # TODO: Updated from `limit`, confirm this is correct variable to use? Limit not in dict for _fill return message @@ -287,15 +347,14 @@ class Telegram(RPCHandler): "Reason: {reason}.".format(**msg)) elif msg_type == RPCMessageType.BUY_FILL: - message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Buy order for {pair} (#{trade_id}) filled " - "for {open_rate}.".format(**msg)) + message = self._format_buy_msg_fill(msg) + elif msg_type == RPCMessageType.SELL_FILL: - message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Sell order for {pair} (#{trade_id}) filled " - "for {close_rate}.".format(**msg)) + message = self._format_sell_msg_fill(msg) + elif msg_type == RPCMessageType.SELL: message = self._format_sell_msg(msg) + elif msg_type == RPCMessageType.PROTECTION_TRIGGER: message = ( "*Protection* triggered due to {reason}. " @@ -357,7 +416,7 @@ class Telegram(RPCHandler): elif float(msg['profit_percent']) >= 0.0: return "\N{EIGHT SPOKED ASTERISK}" elif msg['sell_reason'] == "stop_loss": - return"\N{WARNING SIGN}" + return "\N{WARNING SIGN}" else: return "\N{CROSS MARK}" @@ -689,9 +748,9 @@ class Telegram(RPCHandler): duration_msg = tabulate( [ ['Wins', str(timedelta(seconds=durations['wins'])) - if durations['wins'] != 'N/A' else 'N/A'], + if durations['wins'] != 'N/A' else 'N/A'], ['Losses', str(timedelta(seconds=durations['losses'])) - if durations['losses'] != 'N/A' else 'N/A'] + if durations['losses'] != 'N/A' else 'N/A'] ], headers=['', 'Avg. Duration'] ) @@ -922,9 +981,9 @@ class Telegram(RPCHandler): trade_id = int(context.args[0]) msg = self._rpc._rpc_delete(trade_id) self._send_msg(( - '`{result_msg}`\n' - 'Please make sure to take care of this asset on the exchange manually.' - ).format(**msg)) + '`{result_msg}`\n' + 'Please make sure to take care of this asset on the exchange manually.' + ).format(**msg)) except RPCException as e: self._send_msg(str(e)) @@ -943,7 +1002,7 @@ class Telegram(RPCHandler): output = "Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i+1}.\t {trade['pair']}\t" + f"{i + 1}.\t {trade['pair']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") @@ -978,7 +1037,7 @@ class Telegram(RPCHandler): output = "Buy Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i+1}.\t {trade['buy_tag']}\t" + f"{i + 1}.\t {trade['buy_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") @@ -1013,7 +1072,7 @@ class Telegram(RPCHandler): output = "Sell Reason Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i+1}.\t {trade['sell_reason']}\t" + f"{i + 1}.\t {trade['sell_reason']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") @@ -1048,7 +1107,7 @@ class Telegram(RPCHandler): output = "Mix Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i+1}.\t {trade['mix_tag']}\t" + f"{i + 1}.\t {trade['mix_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit']:.2%}) " f"({trade['count']})\n") From 32e33762963c23dd170729da3bf160b743e5fd55 Mon Sep 17 00:00:00 2001 From: ethan Date: Fri, 12 Nov 2021 21:49:07 -0500 Subject: [PATCH 52/76] Update buy/sell fill telegram notifications --- freqtrade/rpc/telegram.py | 102 +++++++++++++++++---------------- tests/rpc/test_rpc_telegram.py | 61 ++++++++++++-------- 2 files changed, 90 insertions(+), 73 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2a700348f..11574c1f7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -219,6 +219,7 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 + msg['currency'] = msg['pair'].split('/')[0] content = [] content.append( f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" @@ -226,7 +227,7 @@ class Telegram(RPCHandler): ) if msg.get('buy_tag', None): content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f} {msg['pair'].split('/')[0]}`\n") + content.append(f"*Amount:* `{msg['amount']:.8f}` {msg['currency']}\n") content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n") content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n") content.append( @@ -241,13 +242,14 @@ class Telegram(RPCHandler): message += ")`" return message - def _format_buy_msg_fill(self, msg: Dict[str, Any]) -> str: + def _format_buy_fill_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 + msg['currency'] = msg['pair'].split('/')[0] content = [] content.append( f"\N{CHECK MARK} *{msg['exchange']}:* Bought {msg['pair']}" @@ -255,7 +257,8 @@ class Telegram(RPCHandler): ) if msg.get('buy_tag', None): content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f} {msg['pair'].split('/')[0]}`\n") + content.append(f"*Amount:* `{msg['amount']:.8f}` {msg['currency']}\n") + content.append(f"*Open Rate:* `{msg['open_rate']:.8f}`\n") content.append( f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" ) @@ -269,38 +272,6 @@ class Telegram(RPCHandler): return message def _format_sell_msg(self, msg: Dict[str, Any]) -> str: - msg['amount'] = round(msg['amount'], 8) - msg['duration'] = msg['close_date'].replace( - microsecond=0) - msg['open_date'].replace(microsecond=0) - msg['duration_min'] = msg['duration'].total_seconds() / 60 - msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None - - # 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']) - msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) - else: - msg['profit_extra'] = '' - - msg['currency'] = msg['pair'].split('/')[0] - message = ("\N{LARGE RED CIRCLE} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Unrealized Profit:* `{profit_ratio:.2%}{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}` {currency}\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{close_rate:.8f}`").format( - **msg) # TODO: Updated from `limit`, confirm this is correct variable? - - return message - - def _format_sell_msg_fill(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( @@ -320,18 +291,49 @@ class Telegram(RPCHandler): ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) else: msg['profit_extra'] = '' - msg['currency'] = msg['pair'].split('/')[0] + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Unrealized Profit:* `{profit_ratio:.2%}{profit_extra}`\n" + "*Buy Tag:* `{buy_tag}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Amount:* `{amount:.8f}` {currency}\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`").format(**msg) + + return message + + def _format_sell_fill_msg(self, msg: Dict[str, Any]) -> str: import pprint pprint.pprint(msg) + 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['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None + msg['emoji'] = self._get_sell_emoji(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']) + msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}' + ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) + else: + msg['profit_extra'] = '' + msg['currency'] = msg['pair'].split('/')[0] message = ("{emoji} *{exchange}:* Sold {pair} (#{trade_id})\n" "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" "*Buy Tag:* `{buy_tag}`\n" "*Sell Reason:* `{sell_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}` {currency}\n" - "*Close Rate:* `{close_rate:.8f}`").format( - **msg) # TODO: Updated from `limit`, confirm this is correct variable to use? Limit not in dict for _fill + "*Close Rate:* `{close_rate:.8f}`").format(**msg) return message @@ -347,10 +349,10 @@ class Telegram(RPCHandler): "Reason: {reason}.".format(**msg)) elif msg_type == RPCMessageType.BUY_FILL: - message = self._format_buy_msg_fill(msg) + message = self._format_buy_fill_msg(msg) elif msg_type == RPCMessageType.SELL_FILL: - message = self._format_sell_msg_fill(msg) + message = self._format_sell_fill_msg(msg) elif msg_type == RPCMessageType.SELL: message = self._format_sell_msg(msg) @@ -360,11 +362,13 @@ class Telegram(RPCHandler): "*Protection* triggered due to {reason}. " "`{pair}` will be locked until `{lock_end_time}`." ).format(**msg) + elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL: message = ( "*Protection* triggered due to {reason}. " "*All pairs* will be locked until `{lock_end_time}`." ).format(**msg) + elif msg_type == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) @@ -748,9 +752,9 @@ class Telegram(RPCHandler): duration_msg = tabulate( [ ['Wins', str(timedelta(seconds=durations['wins'])) - if durations['wins'] != 'N/A' else 'N/A'], + if durations['wins'] != 'N/A' else 'N/A'], ['Losses', str(timedelta(seconds=durations['losses'])) - if durations['losses'] != 'N/A' else 'N/A'] + if durations['losses'] != 'N/A' else 'N/A'] ], headers=['', 'Avg. Duration'] ) @@ -981,9 +985,9 @@ class Telegram(RPCHandler): trade_id = int(context.args[0]) msg = self._rpc._rpc_delete(trade_id) self._send_msg(( - '`{result_msg}`\n' - 'Please make sure to take care of this asset on the exchange manually.' - ).format(**msg)) + '`{result_msg}`\n' + 'Please make sure to take care of this asset on the exchange manually.' + ).format(**msg)) except RPCException as e: self._send_msg(str(e)) @@ -1002,7 +1006,7 @@ class Telegram(RPCHandler): output = "Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {trade['pair']}\t" + f"{i+1}.\t {trade['pair']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") @@ -1037,7 +1041,7 @@ class Telegram(RPCHandler): output = "Buy Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {trade['buy_tag']}\t" + f"{i+1}.\t {trade['buy_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") @@ -1072,7 +1076,7 @@ class Telegram(RPCHandler): output = "Sell Reason Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {trade['sell_reason']}\t" + f"{i+1}.\t {trade['sell_reason']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") @@ -1107,7 +1111,7 @@ class Telegram(RPCHandler): output = "Mix Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {trade['mix_tag']}\t" + f"{i+1}.\t {trade['mix_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit']:.2%}) " f"({trade['count']})\n") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 68f7457d5..0b4ff968b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1613,7 +1613,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: assert msg_mock.call_args[0][0] \ == '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \ '*Buy Tag:* `buy_signal_01`\n' \ - '*Amount:* `1333.33333333`\n' \ + '*Amount:* `1333.33333333` ETH\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ '*Total:* `(0.00100000 BTC, 12.345 USD)`' @@ -1686,17 +1686,25 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_FILL, - 'buy_tag': 'buy_signal_01', 'trade_id': 1, + 'buy_tag': 'buy_signal_01', 'exchange': 'Binance', - 'pair': 'ETH/USDT', - 'open_rate': 200, - 'stake_amount': 100, - 'amount': 0.5, - 'open_date': arrow.utcnow().datetime + 'pair': 'ETH/BTC', + 'stake_amount': 0.001, + # 'stake_amount_fiat': 0.0, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + 'open_rate': 1.099e-05, + 'amount': 1333.3333333333335, + 'open_date': arrow.utcnow().shift(hours=-1) }) - assert (msg_mock.call_args[0][0] == '\N{LARGE CIRCLE} *Binance:* ' - 'Buy order for ETH/USDT (#1) filled for 200.') + + assert msg_mock.call_args[0][0] \ + == '\N{CHECK MARK} *Binance:* Bought ETH/BTC (#1)\n' \ + '*Buy Tag:* `buy_signal_01`\n' \ + '*Amount:* `1333.33333333` ETH\n' \ + '*Open Rate:* `0.00001099`\n' \ + '*Total:* `(0.00100000 BTC, 12.345 USD)`' def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1727,11 +1735,11 @@ 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' + '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1:00:00 (60.0 min)`\n' - '*Amount:* `1333.33333333`\n' + '*Amount:* `1333.33333333` KEY\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Close Rate:* `0.00003201`' @@ -1759,11 +1767,11 @@ 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' + '*Unrealized Profit:* `-57.41%`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' - '*Amount:* `1333.33333333`\n' + '*Amount:* `1333.33333333` KEY\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Close Rate:* `0.00003201`' @@ -1813,25 +1821,30 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: 'type': RPCMessageType.SELL_FILL, 'trade_id': 1, 'exchange': 'Binance', - 'pair': 'ETH/USDT', + 'pair': 'KEY/ETH', 'gain': 'loss', 'limit': 3.201e-05, - 'amount': 0.1, + 'amount': 1333.3333333333335, 'order_type': 'market', - 'open_rate': 500, - 'close_rate': 550, - 'current_rate': 3.201e-05, + 'open_rate': 7.5e-05, + 'close_rate': 3.201e-05, 'profit_amount': -0.05746268, 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', - 'fiat_currency': 'USD', 'buy_tag': 'buy_signal1', 'sell_reason': SellType.STOP_LOSS.value, - 'open_date': arrow.utcnow().shift(hours=-1), + 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == ('\N{LARGE CIRCLE} *Binance:* Sell order for ETH/USDT (#1) filled for 550.') + == ('\N{WARNING SIGN} *Binance:* Sold KEY/ETH (#1)\n' + '*Profit:* `-57.41%`\n' + '*Buy Tag:* `buy_signal1`\n' + '*Sell Reason:* `stop_loss`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' + '*Amount:* `1333.33333333` KEY\n' + '*Close Rate:* `0.00003201`' + ) def test_send_msg_status_notification(default_conf, mocker) -> None: @@ -1892,7 +1905,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' '*Buy Tag:* `buy_signal_01`\n' - '*Amount:* `1333.33333333`\n' + '*Amount:* `1333.33333333` ETH\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' '*Total:* `(0.00100000 BTC)`') @@ -1923,11 +1936,11 @@ 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' + '*Unrealized Profit:* `-57.41%`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `2:35:03 (155.1 min)`\n' - '*Amount:* `1333.33333333`\n' + '*Amount:* `1333.33333333` KEY\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Close Rate:* `0.00003201`' From df27499e1988c6b40fa08cdbf4838fd62cffb058 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Nov 2021 08:46:06 +0100 Subject: [PATCH 53/76] Improve `/help` output from telegram --- freqtrade/rpc/telegram.py | 86 ++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f53154dff..cb686a47c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1233,44 +1233,56 @@ class Telegram(RPCHandler): forcebuy_text = ("*/forcebuy []:* `Instantly buys the given pair. " "Optionally takes a rate at which to buy " "(only applies to limit orders).` \n") - message = ("*/start:* `Starts the trader`\n" - "*/stop:* `Stops the trader`\n" - "*/status |[table]:* `Lists all open trades`\n" - " * :* `Lists one or more specific trades.`\n" - " `Separate multiple with a blank space.`\n" - " *table :* `will display trades in a table`\n" - " `pending buy orders are marked with an asterisk (*)`\n" - " `pending sell orders are marked with a double asterisk (**)`\n" - "*/buys :* `Shows the buy_tag performance`\n" - "*/sells :* `Shows the sell reason performance`\n" - "*/mix_tags :* `Shows combined buy tag + sell reason performance`\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" - "*/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 ''}" - "*/delete :* `Instantly delete the given trade in the database`\n" - "*/performance:* `Show performance of each finished trade grouped by pair`\n" - "*/daily :* `Shows profit or loss per day, over the last n days`\n" - "*/stats:* `Shows Wins / losses by Sell reason as well as " - "Avg. holding durationsfor buys and sells.`\n" - "*/count:* `Show number of active trades compared to allowed number of trades`\n" - "*/locks:* `Show currently locked pairs`\n" - "*/unlock :* `Unlock this Pair (or this lock id if it's numeric)`\n" - "*/balance:* `Show account balance per currency`\n" - "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" - "*/reload_config:* `Reload configuration file` \n" - "*/show_config:* `Show running configuration` \n" - "*/logs [limit]:* `Show latest logs - defaults to 10` \n" - "*/whitelist:* `Show current whitelist` \n" - "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " - "to the blacklist.` \n" - "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" - "*/help:* `This help message`\n" - "*/version:* `Show version`") + message = ( + "_BotControl_\n" + "------------\n" + "*/start:* `Starts the trader`\n" + "*/stop:* Stops the trader\n" + "*/stopbuy:* `Stops buying, but handles open trades gracefully` \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 ''}" + "*/delete :* `Instantly delete the given trade in the database`\n" + "*/whitelist:* `Show current whitelist` \n" + "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " + "to the blacklist.` \n" + "*/reload_config:* `Reload configuration file` \n" + "*/unlock :* `Unlock this Pair (or this lock id if it's numeric)`\n" - self._send_msg(message) + "_Current state_\n" + "------------\n" + "*/show_config:* `Show running configuration` \n" + "*/locks:* `Show currently locked pairs`\n" + "*/balance:* `Show account balance per currency`\n" + "*/logs [limit]:* `Show latest logs - defaults to 10` \n" + "*/count:* `Show number of active trades compared to allowed number of trades`\n" + "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" + + "_Statistics_\n" + "------------\n" + "*/status |[table]:* `Lists all open trades`\n" + " * :* `Lists one or more specific trades.`\n" + " `Separate multiple with a blank space.`\n" + " *table :* `will display trades in a table`\n" + " `pending buy orders are marked with an asterisk (*)`\n" + " `pending sell orders are marked with a double asterisk (**)`\n" + "*/buys :* `Shows the buy_tag performance`\n" + "*/sells :* `Shows the sell reason performance`\n" + "*/mix_tags :* `Shows combined buy tag + sell reason performance`\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" + "*/performance:* `Show performance of each finished trade grouped by pair`\n" + "*/daily :* `Shows profit or loss per day, over the last n days`\n" + "*/weekly :* `Shows statistics per week, over the last n weeks`\n" + "*/monthly :* `Shows statistics per month, over the last n months`\n" + "*/stats:* `Shows Wins / losses by Sell reason as well as " + "Avg. holding durationsfor buys and sells.`\n" + "*/help:* `This help message`\n" + "*/version:* `Show version`" + ) + + self._send_msg(message, parse_mode=ParseMode.MARKDOWN) @authorized_only def _version(self, update: Update, context: CallbackContext) -> None: From 0e2b5ef6d4280443d33e22cb516b1e3388ff7091 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Nov 2021 09:02:56 +0100 Subject: [PATCH 54/76] Simplify custom strategy file wording --- docs/strategy-customization.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index f3658dd5a..871d763a9 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -4,26 +4,13 @@ This page explains how to customize your strategies, add new indicators and set Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates. -## Install a custom strategy file - -This is very simple. Copy paste your strategy file into the directory `user_data/strategies`. - -Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`: - -1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py` -2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) - -```bash -freqtrade trade --strategy AwesomeStrategy -``` - ## Develop your own strategy The bot includes a default strategy file. Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). You will however most likely have your own idea for a strategy. -This document intends to help you develop one for yourself. +This document intends to help you convert your strategy idea into your own strategy. To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`. This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`. @@ -318,6 +305,19 @@ Currently this is `pair`, which can be accessed using `metadata['pair']` - and w The Metadata-dict should not be modified and does not persist information across multiple calls. Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information) +## Strategy file loading + +By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`. + +Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade with `freqtrade trade --strategy AwesomeStrategy`. +Note that we're using the class-name, not the file name. + +You can use `freqtrade list-strategies` to see a list of all strategies Freqtrade is able to load (all strategies in the correct folder). +It will also include a "status" field, highlighting potential problems. + +??? Hint "Customize strategy directory" + You can use a different directory by using `--strategy-path user_data/otherPath`. This parameter is available to all commands that require a strategy. + ## Informative Pairs ### Get data for non-tradeable pairs From 0e70d23beffbf71e702e2cf825b9aa42c7f11f90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Nov 2021 09:13:32 +0100 Subject: [PATCH 55/76] Add documentation for exit_tags --- docs/strategy-advanced.md | 72 +++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index fcd6138ed..47d7ee6ae 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -143,6 +143,52 @@ def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_r !!! Note `buy_tag` is limited to 100 characters, remaining data will be truncated. +## Exit tag + +Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag. + +``` python +def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe['rsi'] > 70) & + (dataframe['volume'] > 0) + ), + ['sell', 'exit_tag']] = (1, 'exit_rsi') + + return dataframe +``` + +The provided exit-tag is then used as sell-reason - and shown as such in backtest results. + +!!! Note + `sell_reason` is limited to 100 characters, remaining data will be truncated. + +## Bot loop start callback + +A simple callback which is called once at the start of every bot throttling iteration. +This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. + +``` python +import requests + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. gather some remote resource for comparison) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + if self.config['runmode'].value in ('live', 'dry_run'): + # Assign this to the class by using self.* + # can then be used by populate_* methods + self.remote_data = requests.get('https://some_remote_source.example.com') + +``` ## Custom stoploss @@ -501,32 +547,6 @@ class AwesomeStrategy(IStrategy): --- -## Bot loop start callback - -A simple callback which is called once at the start of every bot throttling iteration. -This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. - -``` python -import requests - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - def bot_loop_start(self, **kwargs) -> None: - """ - Called at the start of the bot iteration (one loop). - Might be used to perform pair-independent tasks - (e.g. gather some remote resource for comparison) - :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - """ - if self.config['runmode'].value in ('live', 'dry_run'): - # Assign this to the class by using self.* - # can then be used by populate_* methods - self.remote_data = requests.get('https://some_remote_source.example.com') - -``` - ## Bot order confirmation ### Trade entry (buy order) confirmation From 37d461c6c2a84934923fc3483b407800f08e268a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Nov 2021 11:47:28 +0100 Subject: [PATCH 56/76] Improve strategy dataframe documentation --- docs/strategy-customization.md | 45 +++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 871d763a9..d54bae710 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -12,12 +12,15 @@ Also, several other strategies are available in the [strategy repository](https: You will however most likely have your own idea for a strategy. This document intends to help you convert your strategy idea into your own strategy. -To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`. +To get started, use `freqtrade new-strategy --strategy AwesomeStrategy` (you can obviously use your own naming for your strategy). This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`. !!! Note This is just a template file, which will most likely not be profitable out of the box. +??? Hint "Different template levels" + `freqtrade new-strategy` has an additional parameter, `--template`, which controls the amount of pre-build information you get in the created strategy. Use `--template minimal` to get an empty strategy without any indicator examples, or `--template advanced` to get a template with most callbacks defined. + ### Anatomy of a strategy A strategy file contains all the information needed to build a good strategy: @@ -54,6 +57,46 @@ file as reference.** needs to take care to avoid having the strategy utilize data from the future. Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document. +### Dataframe + +Freqtrade uses [pandas](https://pandas.pydata.org/) to store/provide the candlestick (OHLCV) data. +Pandas is a great library developed for processing large amounts of data. + +Each row in a dataframe corresponds to one candle on a chart, with the latest candle always being the last in the dataframe (sorted by date). + +``` output +> dataframe.head() + date open high low close volume +0 2021-11-09 23:25:00+00:00 67279.67 67321.84 67255.01 67300.97 44.62253 +1 2021-11-09 23:30:00+00:00 67300.97 67301.34 67183.03 67187.01 61.38076 +2 2021-11-09 23:35:00+00:00 67187.02 67187.02 67031.93 67123.81 113.42728 +3 2021-11-09 23:40:00+00:00 67123.80 67222.40 67080.33 67160.48 78.96008 +4 2021-11-09 23:45:00+00:00 67160.48 67160.48 66901.26 66943.37 111.39292 +``` + +Pandas provides fast ways to calculate metrics. To benefit from this speed, it's advised to not use loops, but use vectorized methods instead. + +Vectorized operations perform calculations across the whole range of data and are therefore, compared to looping through each row, a lot faster when calculating indicators. + +As a dataframe is a table, simple python comparisons like the following will not work + +``` python + if dataframe['rsi'] > 30: + dataframe['buy'] = 1 +``` + +The above section will fail with `The truth value of a Series is ambiguous. [...]`. + +This must instead be written in a pandas-compatible way, so the operation is performed across the whole dataframe. + +``` python + dataframe.loc[ + (dataframe['rsi'] > 30) + , 'buy'] = 1 +``` + +With this section, you have a new column in your dataframe, which has `1` assigned whenever RSI is above 30. + ### Customize Indicators Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file. From a177e58dc4d7b64d082136ae27233fb778318646 Mon Sep 17 00:00:00 2001 From: ethan Date: Sat, 13 Nov 2021 08:49:02 -0500 Subject: [PATCH 57/76] Remove "currency" generating from splitting pair str --- freqtrade/rpc/telegram.py | 14 ++++++-------- tests/rpc/test_rpc_telegram.py | 14 +++++++------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 11574c1f7..7bb5b71f9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -219,7 +219,6 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - msg['currency'] = msg['pair'].split('/')[0] content = [] content.append( f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" @@ -227,7 +226,7 @@ class Telegram(RPCHandler): ) if msg.get('buy_tag', None): content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f}` {msg['currency']}\n") + content.append(f"*Amount:* `{msg['amount']:.8f}`\n") content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n") content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n") content.append( @@ -249,7 +248,6 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - msg['currency'] = msg['pair'].split('/')[0] content = [] content.append( f"\N{CHECK MARK} *{msg['exchange']}:* Bought {msg['pair']}" @@ -257,7 +255,7 @@ class Telegram(RPCHandler): ) if msg.get('buy_tag', None): content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f}` {msg['currency']}\n") + content.append(f"*Amount:* `{msg['amount']:.8f}`\n") content.append(f"*Open Rate:* `{msg['open_rate']:.8f}`\n") content.append( f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" @@ -291,13 +289,13 @@ class Telegram(RPCHandler): ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) else: msg['profit_extra'] = '' - msg['currency'] = msg['pair'].split('/')[0] + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" "*Unrealized Profit:* `{profit_ratio:.2%}{profit_extra}`\n" "*Buy Tag:* `{buy_tag}`\n" "*Sell Reason:* `{sell_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}` {currency}\n" + "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" "*Close Rate:* `{limit:.8f}`").format(**msg) @@ -326,13 +324,13 @@ class Telegram(RPCHandler): ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) else: msg['profit_extra'] = '' - msg['currency'] = msg['pair'].split('/')[0] + message = ("{emoji} *{exchange}:* Sold {pair} (#{trade_id})\n" "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" "*Buy Tag:* `{buy_tag}`\n" "*Sell Reason:* `{sell_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}` {currency}\n" + "*Amount:* `{amount:.8f}`\n" "*Close Rate:* `{close_rate:.8f}`").format(**msg) return message diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0b4ff968b..95a2f4549 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1613,7 +1613,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: assert msg_mock.call_args[0][0] \ == '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \ '*Buy Tag:* `buy_signal_01`\n' \ - '*Amount:* `1333.33333333` ETH\n' \ + '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ '*Total:* `(0.00100000 BTC, 12.345 USD)`' @@ -1702,7 +1702,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] \ == '\N{CHECK MARK} *Binance:* Bought ETH/BTC (#1)\n' \ '*Buy Tag:* `buy_signal_01`\n' \ - '*Amount:* `1333.33333333` ETH\n' \ + '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Total:* `(0.00100000 BTC, 12.345 USD)`' @@ -1739,7 +1739,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1:00:00 (60.0 min)`\n' - '*Amount:* `1333.33333333` KEY\n' + '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Close Rate:* `0.00003201`' @@ -1771,7 +1771,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' - '*Amount:* `1333.33333333` KEY\n' + '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Close Rate:* `0.00003201`' @@ -1842,7 +1842,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' - '*Amount:* `1333.33333333` KEY\n' + '*Amount:* `1333.33333333`\n' '*Close Rate:* `0.00003201`' ) @@ -1905,7 +1905,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' '*Buy Tag:* `buy_signal_01`\n' - '*Amount:* `1333.33333333` ETH\n' + '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' '*Total:* `(0.00100000 BTC)`') @@ -1940,7 +1940,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `2:35:03 (155.1 min)`\n' - '*Amount:* `1333.33333333` KEY\n' + '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Close Rate:* `0.00003201`' From 7412b7ba5185b9a65963140209f142462e6ed6ca Mon Sep 17 00:00:00 2001 From: ethan Date: Sat, 13 Nov 2021 10:23:47 -0500 Subject: [PATCH 58/76] buy/sell fill notification code consolidation --- freqtrade/rpc/telegram.py | 150 +++++++++++++++----------------------- 1 file changed, 58 insertions(+), 92 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7bb5b71f9..63174024e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -220,53 +220,48 @@ class Telegram(RPCHandler): msg['stake_amount_fiat'] = 0 content = [] - content.append( - f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" - f" (#{msg['trade_id']})\n" - ) - if msg.get('buy_tag', None): - content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f}`\n") - content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n") - content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n") - content.append( - f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" - ) - if msg.get('fiat_currency', None): + # Buy Order Fill + if msg['type'] == RPCMessageType.BUY_FILL: content.append( - f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + f"\N{CHECK MARK} *{msg['exchange']}:* Bought {msg['pair']}" + f" (#{msg['trade_id']})\n" ) + if msg.get('buy_tag', None): + content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") + content.append(f"*Amount:* `{msg['amount']:.8f}`\n") + content.append(f"*Open Rate:* `{msg['open_rate']:.8f}`\n") + content.append( + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" + ) + if msg.get('fiat_currency', None): + content.append( + f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + ) - message = ''.join(content) - message += ")`" - return message - - def _format_buy_fill_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']) + message = ''.join(content) + message += ")`" + # Buy Order Ask else: - msg['stake_amount_fiat'] = 0 - - content = [] - content.append( - f"\N{CHECK MARK} *{msg['exchange']}:* Bought {msg['pair']}" - f" (#{msg['trade_id']})\n" - ) - if msg.get('buy_tag', None): - content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f}`\n") - content.append(f"*Open Rate:* `{msg['open_rate']:.8f}`\n") - content.append( - f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" - ) - if msg.get('fiat_currency', None): content.append( - f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" + f" (#{msg['trade_id']})\n" ) + if msg.get('buy_tag', None): + content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") + content.append(f"*Amount:* `{msg['amount']:.8f}`\n") + content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n") + content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n") + content.append( + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" + ) + if msg.get('fiat_currency', None): + content.append( + f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + ) + + message = ''.join(content) + message += ")`" - message = ''.join(content) - message += ")`" return message def _format_sell_msg(self, msg: Dict[str, Any]) -> str: @@ -290,71 +285,42 @@ class Telegram(RPCHandler): else: msg['profit_extra'] = '' - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Unrealized Profit:* `{profit_ratio:.2%}{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\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) + # Sell Order Fill + if msg['type'] == RPCMessageType.SELL_FILL: + message = ("{emoji} *{exchange}:* Sold {pair} (#{trade_id})\n" + "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" + "*Buy Tag:* `{buy_tag}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Amount:* `{amount:.8f}`\n" + "*Close Rate:* `{close_rate:.8f}`").format(**msg) - return message - - def _format_sell_fill_msg(self, msg: Dict[str, Any]) -> str: - import pprint - pprint.pprint(msg) - 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['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None - msg['emoji'] = self._get_sell_emoji(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']) - msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})').format(**msg) + # Sell Order Ask else: - msg['profit_extra'] = '' - - message = ("{emoji} *{exchange}:* Sold {pair} (#{trade_id})\n" - "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n" - "*Close Rate:* `{close_rate:.8f}`").format(**msg) - + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Unrealized Profit:* `{profit_ratio:.2%}{profit_extra}`\n" + "*Buy Tag:* `{buy_tag}`\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 compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: - - if msg_type == RPCMessageType.BUY: + if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL]: message = self._format_buy_msg(msg) + elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]: + message = self._format_sell_msg(msg) + 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: - message = self._format_buy_fill_msg(msg) - - elif msg_type == RPCMessageType.SELL_FILL: - message = self._format_sell_fill_msg(msg) - - elif msg_type == RPCMessageType.SELL: - message = self._format_sell_msg(msg) - elif msg_type == RPCMessageType.PROTECTION_TRIGGER: message = ( "*Protection* triggered due to {reason}. " From 0bc9384451c22c70f3ad805b3701dec1d217838b Mon Sep 17 00:00:00 2001 From: ethan Date: Sat, 13 Nov 2021 14:52:59 -0500 Subject: [PATCH 59/76] more notification code consolidation --- freqtrade/rpc/telegram.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 63174024e..36bbf1a64 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -285,27 +285,22 @@ class Telegram(RPCHandler): else: msg['profit_extra'] = '' - # Sell Order Fill - if msg['type'] == RPCMessageType.SELL_FILL: - message = ("{emoji} *{exchange}:* Sold {pair} (#{trade_id})\n" - "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n" - "*Close Rate:* `{close_rate:.8f}`").format(**msg) + message = ("{emoji} *{exchange}:* Sold {pair} (#{trade_id})\n" + "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" + "*Buy Tag:* `{buy_tag}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Amount:* `{amount:.8f}`\n").format(**msg) + + if msg['type'] == RPCMessageType.SELL: + message = message.replace('Sold', 'Selling').replace('Profit', 'Unrealized Profit') + message += ("*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`").format(**msg) + + elif msg['type'] == RPCMessageType.SELL_FILL: + message += ("*Close Rate:* `{close_rate:.8f}`").format(**msg) - # Sell Order Ask - else: - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Unrealized Profit:* `{profit_ratio:.2%}{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\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 compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: From c4c1b301cde2418026b4b69aa2be54b58ddc9ba5 Mon Sep 17 00:00:00 2001 From: ethan Date: Sat, 13 Nov 2021 15:46:00 -0500 Subject: [PATCH 60/76] buy notification code consolidation --- freqtrade/rpc/telegram.py | 54 +++++++++++---------------------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 36bbf1a64..c678309aa 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -219,49 +219,25 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - content = [] - # Buy Order Fill + message = "\N{CHECK MARK} *{exchange}:* Bought {pair} (#{trade_id})\n".format(**msg) + message += "*Buy Tag:* `{buy_tag}`\n".format(**msg) if msg.get('buy_tag', None) else "" + message += "*Amount:* `{amount:.8f}`\n".format(**msg) + if msg['type'] == RPCMessageType.BUY_FILL: - content.append( - f"\N{CHECK MARK} *{msg['exchange']}:* Bought {msg['pair']}" - f" (#{msg['trade_id']})\n" - ) - if msg.get('buy_tag', None): - content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f}`\n") - content.append(f"*Open Rate:* `{msg['open_rate']:.8f}`\n") - content.append( - f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" - ) - if msg.get('fiat_currency', None): - content.append( - f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" - ) + message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n" - message = ''.join(content) - message += ")`" - # Buy Order Ask - else: - content.append( - f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" - f" (#{msg['trade_id']})\n" - ) - if msg.get('buy_tag', None): - content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") - content.append(f"*Amount:* `{msg['amount']:.8f}`\n") - content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n") - content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n") - content.append( - f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" - ) - if msg.get('fiat_currency', None): - content.append( - f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" - ) + elif msg['type'] == RPCMessageType.BUY: + message = message.replace('Bought', 'Buying')\ + .replace("\N{CHECK MARK}", "\N{LARGE BLUE CIRCLE}") + message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\ + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - message = ''.join(content) - message += ")`" + message += 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: From 7c1161992467cf319543cbc6095b7e2decc970fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Nov 2021 09:19:21 +0100 Subject: [PATCH 61/76] avoid using replace in messages --- freqtrade/rpc/telegram.py | 45 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c678309aa..e9c2c23a1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -28,6 +28,7 @@ from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException, RPCHandler + logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') @@ -218,17 +219,20 @@ class Telegram(RPCHandler): msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) else: msg['stake_amount_fiat'] = 0 + is_fill = msg['type'] == RPCMessageType.BUY_FILL + emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}' - message = "\N{CHECK MARK} *{exchange}:* Bought {pair} (#{trade_id})\n".format(**msg) - message += "*Buy Tag:* `{buy_tag}`\n".format(**msg) if msg.get('buy_tag', None) else "" - message += "*Amount:* `{amount:.8f}`\n".format(**msg) + message = ( + f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}" + f" (#{msg['trade_id']})\n" + ) + message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else "" + message += f"*Amount:* `{msg['amount']:.8f}`\n" if msg['type'] == RPCMessageType.BUY_FILL: message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n" elif msg['type'] == RPCMessageType.BUY: - message = message.replace('Bought', 'Buying')\ - .replace("\N{CHECK MARK}", "\N{LARGE BLUE CIRCLE}") message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\ f"*Current Rate:* `{msg['current_rate']:.8f}`\n" @@ -256,26 +260,29 @@ class Telegram(RPCHandler): and self._rpc._fiat_converter): 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) + msg['profit_extra'] = ( + f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" + f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})") else: msg['profit_extra'] = '' - - message = ("{emoji} *{exchange}:* Sold {pair} (#{trade_id})\n" - "*Profit:* `{profit_ratio:.2%}{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n").format(**msg) + is_fill = msg['type'] == RPCMessageType.SELL_FILL + message = ( + f"{msg['emoji']} *{msg['exchange']}:* " + f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n" + f"*{'Profit' if is_fill else 'Unrealized Profit'}:* " + f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" + f"*Buy Tag:* `{msg['buy_tag']}`\n" + f"*Sell Reason:* `{msg['sell_reason']}`\n" + f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n" + f"*Amount:* `{msg['amount']:.8f}`\n") if msg['type'] == RPCMessageType.SELL: - message = message.replace('Sold', 'Selling').replace('Profit', 'Unrealized Profit') - message += ("*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`").format(**msg) + message += (f"*Open Rate:* `{msg['open_rate']:.8f}`\n" + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + f"*Close Rate:* `{msg['limit']:.8f}`") elif msg['type'] == RPCMessageType.SELL_FILL: - message += ("*Close Rate:* `{close_rate:.8f}`").format(**msg) + message += f"*Close Rate:* `{msg['close_rate']:.8f}`" return message From 632c1bc0aa8eec3953318b2d50efe6d2dc632f57 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Mon, 14 Jun 2021 14:39:12 +0900 Subject: [PATCH 62/76] Add static workaround for kucoin 429000 issue closes #5700 --- freqtrade/exchange/common.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 644a13e93..a4c827e07 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -81,9 +81,16 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) if isinstance(ex, DDosProtection): - backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) - logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") - await asyncio.sleep(backoff_delay) + if "kucoin" in str(ex) and "429000" in str(ex): + # Temporary fix for 429000 error on kucoin + # see https://github.com/freqtrade/freqtrade/issues/5700 for details. + logger.warning( + f"Kucoin 429 error, avoid triggering DDosProtection backoff delay. " + f"{count} tries left before giving up") + else: + backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) + logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") + await asyncio.sleep(backoff_delay) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) From 5b9cbaf2774de9d4cdb34797108f1cf691295dc4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Nov 2021 09:50:04 +0100 Subject: [PATCH 63/76] Use Close value for trade signal calculation --- freqtrade/rpc/rpc.py | 6 +++--- tests/rpc/test_rpc_apiserver.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index de066d123..28585e4e8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -909,15 +909,15 @@ class RPC: if has_content: dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000 - # Move open to separate column when signal for easy plotting + # Move signal close to separate column when signal for easy plotting if 'buy' in dataframe.columns: buy_mask = (dataframe['buy'] == 1) buy_signals = int(buy_mask.sum()) - dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open'] + dataframe.loc[buy_mask, '_buy_signal_close'] = dataframe.loc[buy_mask, 'close'] if 'sell' in dataframe.columns: sell_mask = (dataframe['sell'] == 1) sell_signals = int(sell_mask.sum()) - dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] + dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close'] dataframe = dataframe.replace([inf, -inf], NAN) dataframe = dataframe.replace({NAN: None}) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e650e5142..473df8b19 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1129,7 +1129,7 @@ def test_api_pair_candles(botclient, ohlcv_history): assert isinstance(rc.json()['columns'], list) assert rc.json()['columns'] == ['date', 'open', 'high', 'low', 'close', 'volume', 'sma', 'buy', 'sell', - '__date_ts', '_buy_signal_open', '_sell_signal_open'] + '__date_ts', '_buy_signal_close', '_sell_signal_close'] assert 'pair' in rc.json() assert rc.json()['pair'] == 'XRP/BTC' @@ -1140,7 +1140,7 @@ def test_api_pair_candles(botclient, ohlcv_history): [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, None, 0, 0, 1511686200000, None, None], ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None], + 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.893e-05, None], ['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, 0.7039405, 8.885e-05, 0, 0, 1511686800000, None, None] From 1dc98cc4d5f15b779e9b43e0634262edc464ef09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Nov 2021 10:12:19 +0100 Subject: [PATCH 64/76] Break line --- tests/rpc/test_rpc_apiserver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 473df8b19..074e312d9 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1140,7 +1140,8 @@ def test_api_pair_candles(botclient, ohlcv_history): [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, None, 0, 0, 1511686200000, None, None], ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.893e-05, None], + 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.893e-05, + None], ['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, 0.7039405, 8.885e-05, 0, 0, 1511686800000, None, None] From 4d1d8de9b7deb978d892f4ef21b0292e3cba4eec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Nov 2021 10:20:04 +0100 Subject: [PATCH 65/76] Split /stats messages closes #5869 --- freqtrade/rpc/telegram.py | 14 ++++++++++---- tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1db28546e..0e1a6fe27 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -686,10 +686,16 @@ class Telegram(RPCHandler): count['losses'] ] for reason, count in stats['sell_reasons'].items() ] - sell_reasons_msg = tabulate( - sell_reasons_tabulate, - headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] - ) + sell_reasons_msg = 'No trades yet.' + for reason in chunks(sell_reasons_tabulate, 25): + sell_reasons_msg = tabulate( + reason, + headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] + ) + if len(sell_reasons_tabulate) > 25: + self._send_msg(sell_reasons_msg, ParseMode.MARKDOWN) + sell_reasons_msg = '' + durations = stats['durations'] duration_msg = tabulate( [ diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 95a2f4549..1247affae 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -731,7 +731,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - # assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] + assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() # Create some test data From 43120e03f95295bda973f3e52717d1e284a3e624 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 03:01:00 +0000 Subject: [PATCH 66/76] Bump types-requests from 2.25.11 to 2.26.0 Bumps [types-requests](https://github.com/python/typeshed) from 2.25.11 to 2.26.0. - [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-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 a66815daa..7c7019856 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ nbconvert==6.2.0 # mypy types types-cachetools==4.2.4 types-filelock==3.2.1 -types-requests==2.25.11 +types-requests==2.26.0 types-tabulate==0.8.3 # Extensions to datetime library From c04679072785c9e73ada29deb610160d76721c92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 03:01:04 +0000 Subject: [PATCH 67/76] Bump pymdown-extensions from 9.0 to 9.1 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.0 to 9.1. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.0...9.1) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 40269b109..772919436 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 mkdocs-material==7.3.6 mdx_truly_sane_lists==1.2 -pymdown-extensions==9.0 +pymdown-extensions==9.1 From 2a1c61fb3080c1b47fe04c4e4c6ab96c0721a2ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 03:01:22 +0000 Subject: [PATCH 68/76] Bump sqlalchemy from 1.4.26 to 1.4.27 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.26 to 1.4.27. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/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 a252aab42..3aa613e5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.60.68 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.26 +SQLAlchemy==1.4.27 python-telegram-bot==13.7 arrow==1.2.1 cachetools==4.2.2 From 178e3ac6af2db60f4901fc3cd2b014922beefe3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 03:01:27 +0000 Subject: [PATCH 69/76] Bump coveralls from 3.3.0 to 3.3.1 Bumps [coveralls](https://github.com/TheKevJames/coveralls-python) from 3.3.0 to 3.3.1. - [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.3.0...3.3.1) --- updated-dependencies: - dependency-name: coveralls 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 a66815daa..a9fcc2675 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==3.3.0 +coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.5.0 mypy==0.910 From 8ec5f72be475003b5834abda0f0242ab76b0c064 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 03:01:30 +0000 Subject: [PATCH 70/76] Bump isort from 5.10.0 to 5.10.1 Bumps [isort](https://github.com/pycqa/isort) from 5.10.0 to 5.10.1. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.10.0...5.10.1) --- updated-dependencies: - dependency-name: isort 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 a66815daa..86a9ac8bc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,7 +12,7 @@ pytest-asyncio==0.16.0 pytest-cov==3.0.0 pytest-mock==3.6.1 pytest-random-order==1.0.4 -isort==5.10.0 +isort==5.10.1 # For datetime mocking time-machine==2.4.0 From 876b59f4778f1c0cc0206778a3e09e9d91362a1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 03:01:42 +0000 Subject: [PATCH 71/76] Bump jinja2 from 3.0.2 to 3.0.3 Bumps [jinja2](https://github.com/pallets/jinja) from 3.0.2 to 3.0.3. - [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.2...3.0.3) --- updated-dependencies: - dependency-name: jinja2 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 a252aab42..1c9e14b11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ TA-Lib==0.4.21 technical==1.3.0 tabulate==0.8.9 pycoingecko==2.2.0 -jinja2==3.0.2 +jinja2==3.0.3 tables==3.6.1 blosc==1.10.6 From 7bd384c7fb34c02b59ba8c10f42bb2d0ef8c33e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 03:01:45 +0000 Subject: [PATCH 72/76] Bump nbconvert from 6.2.0 to 6.3.0 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.2.0 to 6.3.0. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.2.0...6.3.0) --- updated-dependencies: - dependency-name: nbconvert 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 a66815daa..bdaf93635 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,7 +17,7 @@ isort==5.10.0 time-machine==2.4.0 # Convert jupyter notebooks to markdown documents -nbconvert==6.2.0 +nbconvert==6.3.0 # mypy types types-cachetools==4.2.4 From e3bb102dc0d36560f5c92e6da3073f5930450c8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 05:27:49 +0000 Subject: [PATCH 73/76] Bump python-telegram-bot from 13.7 to 13.8.1 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.7 to 13.8.1. - [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.7...v13.8.1) --- 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 3aa613e5c..e2b37bd06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ ccxt==1.60.68 cryptography==35.0.0 aiohttp==3.7.4.post0 SQLAlchemy==1.4.27 -python-telegram-bot==13.7 +python-telegram-bot==13.8.1 arrow==1.2.1 cachetools==4.2.2 requests==2.26.0 From d477ccab19e992d5273df748c541de98dbc12d3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 05:27:50 +0000 Subject: [PATCH 74/76] Bump ccxt from 1.60.68 to 1.61.24 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.60.68 to 1.61.24. - [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.60.68...1.61.24) --- 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 3aa613e5c..7eacb4b00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.4 pandas==1.3.4 pandas-ta==0.3.14b -ccxt==1.60.68 +ccxt==1.61.24 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 From 60cf52aa34c49a5a64b9c1da9335801b09e4d744 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Nov 2021 07:09:21 +0100 Subject: [PATCH 75/76] Remove unused test code --- tests/conftest.py | 7 +------ tests/exchange/test_exchange.py | 9 --------- tests/test_freqtradebot.py | 8 -------- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 751cb5f9f..3ce064ee3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ from telegram import Chat, Message, Update 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.edge import PairInfo from freqtrade.enums import RunMode from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot @@ -140,11 +140,6 @@ def patch_edge(mocker) -> None: mocker.patch('freqtrade.edge.Edge.calculate', MagicMock(return_value=True)) -def get_patched_edge(mocker, config) -> Edge: - patch_edge(mocker) - edge = Edge(config) - return edge - # Functions for recurrent object patching diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ced7d8d36..12b11ff3d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2017,15 +2017,6 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog): assert exchange.get_rate(pair, refresh=True, side="sell") == 0.13 -def make_fetch_ohlcv_mock(data): - def fetch_ohlcv_mock(pair, timeframe, since): - if since: - assert since > data[-1][0] - return [] - return data - return fetch_ohlcv_mock - - @pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.asyncio async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 629fed76b..e5dae5461 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -170,12 +170,8 @@ def test_edge_called_in_process(mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_edge(mocker) - def _refresh_whitelist(list): - return ['ETH/USDT', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'] - patch_exchange(mocker) freqtrade = FreqtradeBot(edge_conf) - freqtrade.pairlists._validate_whitelist = _refresh_whitelist patch_get_signal(freqtrade) freqtrade.process() assert freqtrade.active_pair_whitelist == ['NEO/BTC', 'LTC/BTC'] @@ -664,9 +660,6 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) patch_RPCManager(mocker) patch_exchange(mocker) - def _refresh_whitelist(list): - return ['ETH/USDT', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'] - refresh_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -682,7 +675,6 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) mocker.patch('time.sleep', return_value=None) freqtrade = FreqtradeBot(default_conf_usdt) - freqtrade.pairlists._validate_whitelist = _refresh_whitelist freqtrade.strategy.informative_pairs = inf_pairs # patch_get_signal(freqtrade) From 5fb0f535391c0e0f8fd15a6f7f43d2b2357c591a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Nov 2021 19:36:38 +0100 Subject: [PATCH 76/76] Add curl to install description --- docs/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index d468786d3..ee7ffe55d 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-dev python3-pandas git + sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git curl ``` === "RaspberryPi/Raspbian" @@ -71,7 +71,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces ```bash - sudo apt-get install python3-venv libatlas-base-dev cmake + sudo apt-get install python3-venv libatlas-base-dev cmake curl # Use pywheels.org to speed up installation sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf