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:
parent
d99eaccb5a
commit
5f40158c0b
@ -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()
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user