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 <antoine.merino.dev@gmail.com>
This commit is contained in:
Antoine Merino 2021-11-05 20:24:40 +01:00
parent 5f40158c0b
commit 459ff9692d
No known key found for this signature in database
GPG Key ID: E53AF74E9DC0C53E
4 changed files with 70 additions and 26 deletions

View File

@ -4,12 +4,12 @@ This module contains class to define a RPC communications
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import date, datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
from dateutil.relativedelta import relativedelta
from math import isnan from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
import arrow import arrow
import psutil import psutil
from dateutil.relativedelta import relativedelta
from numpy import NAN, inf, int64, mean from numpy import NAN, inf, int64, mean
from pandas import DataFrame from pandas import DataFrame
@ -294,13 +294,14 @@ class RPC:
self, timescale: int, self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
today = datetime.utcnow().date() today = datetime.utcnow().date()
first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday
profit_weeks: Dict[date, Dict] = {} profit_weeks: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('timescale must be an integer greater than 0') raise RPCException('timescale must be an integer greater than 0')
for week in range(0, timescale): 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=[ trades = Trade.get_trades(trade_filter=[
Trade.is_open.is_(False), Trade.is_open.is_(False),
Trade.close_date >= profitweek, Trade.close_date >= profitweek,
@ -335,14 +336,14 @@ class RPC:
def _rpc_monthly_profit( def _rpc_monthly_profit(
self, timescale: int, self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: 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] = {} profit_months: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('timescale must be an integer greater than 0') raise RPCException('timescale must be an integer greater than 0')
for month in range(0, timescale): 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=[ trades = Trade.get_trades(trade_filter=[
Trade.is_open.is_(False), Trade.is_open.is_(False),
Trade.close_date >= profitmonth, Trade.close_date >= profitmonth,
@ -357,7 +358,7 @@ class RPC:
data = [ data = [
{ {
'date': key, 'date': f"{key.year}-{key.month}",
'abs_profit': value["amount"], 'abs_profit': value["amount"],
'fiat_value': self._fiat_converter.convert_amount( 'fiat_value': self._fiat_converter.convert_amount(
value['amount'], value['amount'],

View File

@ -492,7 +492,7 @@ class Telegram(RPCHandler):
f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{day['trade_count']} trades"] for day in stats['data']], f"{day['trade_count']} trades"] for day in stats['data']],
headers=[ headers=[
'Month', 'Day',
f'Profit {stake_cur}', f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}', f'Profit {fiat_disp_cur}',
'Trades', 'Trades',
@ -531,13 +531,14 @@ class Telegram(RPCHandler):
f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{week['trade_count']} trades"] for week in stats['data']], f"{week['trade_count']} trades"] for week in stats['data']],
headers=[ headers=[
'Week', 'Monday',
f'Profit {stake_cur}', f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}', f'Profit {fiat_disp_cur}',
'Trades', 'Trades',
], ],
tablefmt='simple') tablefmt='simple')
message = f'<b>Weekly Profit over the last {timescale} weeks</b>:\n<pre>{stats_tab}</pre>' message = f'<b>Weekly Profit over the last {timescale} weeks ' \
f'(starting from Monday)</b>:\n<pre>{stats_tab}</pre> '
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path="update_weekly", query=update.callback_query) callback_path="update_weekly", query=update.callback_query)
except RPCException as e: except RPCException as e:
@ -576,7 +577,8 @@ class Telegram(RPCHandler):
'Trades', 'Trades',
], ],
tablefmt='simple') tablefmt='simple')
message = f'<b>Monthly Profit over the last {timescale} months</b>:\n<pre>{stats_tab}</pre>' message = f'<b>Monthly Profit over the last {timescale} months' \
f'</b>:\n<pre>{stats_tab}</pre> '
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path="update_monthly", query=update.callback_query) callback_path="update_monthly", query=update.callback_query)
except RPCException as e: except RPCException as e:

View File

@ -43,4 +43,6 @@ colorama==0.4.4
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.21 prompt-toolkit==3.0.21
# Extensions to datetime library
types-python-dateutil==2.8.2
python-dateutil==2.8.2 python-dateutil==2.8.2

View File

@ -5,7 +5,6 @@
import logging import logging
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from functools import reduce from functools import reduce
from random import choice, randint from random import choice, randint
from string import ascii_uppercase from string import ascii_uppercase
@ -13,6 +12,7 @@ from unittest.mock import ANY, MagicMock
import arrow import arrow
import pytest import pytest
from dateutil.relativedelta import relativedelta
from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram import Chat, Message, ReplyKeyboardMarkup, Update
from telegram.error import BadRequest, NetworkError, TelegramError 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"] context.args = ["2"]
telegram._daily(update=update, context=context) telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1 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</b>:" 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(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.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(' 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 = [] context.args = []
telegram._daily(update=update, context=context) telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1 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</b>:" 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(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.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(' 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 = MagicMock()
context.args = ["today"] context.args = ["today"]
telegram._daily(update=update, context=context) 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</b>:') in msg_mock.call_args_list[0][0][0]
def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, 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"] context.args = ["2"]
telegram._weekly(update=update, context=context) telegram._weekly(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Weekly' in msg_mock.call_args_list[0][0][0] assert "Weekly Profit over the last 2 weeks (starting from Monday)</b>:" \
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] 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.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(' 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(' 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 = [] context.args = []
telegram._weekly(update=update, context=context) telegram._weekly(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert "Weekly Profit over the last 8 weeks (starting from Monday)</b>:" \
in msg_mock.call_args_list[0][0][0]
assert 'Weekly' 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.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(' 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(' 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"] context.args = ["10"]
telegram._weekly(update=update, context=context) telegram._weekly(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert "Weekly Profit over the last 10 weeks (starting from Monday)</b>:" \
a = msg_mock.call_args_list[0][0][0] in msg_mock.call_args_list[0][0][0]
# Now, the time-shifted trade should be included # 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.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(' 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 # Try invalid data
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
# /daily today # /weekly this week
context = MagicMock() context = MagicMock()
context.args = ["this week"] context.args = ["this week"]
telegram._weekly(update=update, context=context) 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)</b>:') \
in msg_mock.call_args_list[0][0][0]
def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, 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"] context.args = ["2"]
telegram._monthly(update=update, context=context) telegram._monthly(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Monthly' in msg_mock.call_args_list[0][0][0] assert 'Monthly Profit over the last 2 months</b>:' in msg_mock.call_args_list[0][0][0]
assert str(datetime.utcnow().date()) 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.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(' 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(' 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 = [] context.args = []
telegram._monthly(update=update, context=context) telegram._monthly(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Monthly' in msg_mock.call_args_list[0][0][0] assert 'Monthly Profit over the last 6 months</b>:' in msg_mock.call_args_list[0][0][0]
assert str(datetime.utcnow().date()) 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.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(' 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(' 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].open_date = datetime.utcnow() - relativedelta(months=6, days=5)
trades[0].close_date = datetime.utcnow() - relativedelta(months=6, days=3) trades[0].close_date = datetime.utcnow() - relativedelta(months=6, days=3)
# /weekly # /monthly
# By default, the 6 previous months are shown # By default, the 6 previous months are shown
# So the previous modified trade should be excluded from the stats # So the previous modified trade should be excluded from the stats
context = MagicMock() context = MagicMock()
@ -653,13 +664,41 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
telegram._monthly(update=update, context=context) telegram._monthly(update=update, context=context)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
a = msg_mock.call_args_list[0][0][0]
# Now, the time-shifted trade should be included # 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.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(' 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(' 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</b>:') in msg_mock.call_args_list[0][0][0]
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None: limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)