From ffc2de8d3310255ad6b8c94bbe3f4c833a374cd2 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Thu, 4 Nov 2021 20:47:01 +0100 Subject: [PATCH 01/12] 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 15616d75ad8a4c4334fac60dc7160802b5ce83fa Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Fri, 5 Nov 2021 20:24:40 +0100 Subject: [PATCH 02/12] 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 03/12] /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 04/12] 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 05/12] 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 06/12] /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 07/12] 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 08/12] 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 09/12] 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 3c33b48fd5fe21d195c17fd5948200c07920fe33 Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Sat, 6 Nov 2021 13:09:15 +0100 Subject: [PATCH 10/12] 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 11/12] 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 d0e192e20f54322c868c9ffaa4778286429d479b Mon Sep 17 00:00:00 2001 From: Antoine Merino Date: Sat, 6 Nov 2021 13:14:15 +0100 Subject: [PATCH 12/12] 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):