WIP Add /weekly and /monthly to Telegram RPC

Related to "Show average profit in overview" (#5527)

Signed-off-by: Antoine Merino <antoine.merino.dev@gmail.com>
This commit is contained in:
Antoine Merino 2021-11-04 20:47:01 +01:00
parent d99eaccb5a
commit 5f40158c0b
No known key found for this signature in database
GPG Key ID: E53AF74E9DC0C53E
4 changed files with 409 additions and 6 deletions

View File

@ -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()

View File

@ -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 <n>
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'<b>Weekly Profit over the last {timescale} weeks</b>:\n<pre>{stats_tab}</pre>'
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 <n>
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'<b>Monthly Profit over the last {timescale} months</b>:\n<pre>{stats_tab}</pre>'
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:
"""

View File

@ -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

View File

@ -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)