Merge pull request #6957 from freqtrade/rpc_consolidate_daily
Rpc consolidate daily
This commit is contained in:
commit
2218313f5c
@ -86,7 +86,7 @@ def stats(rpc: RPC = Depends(get_rpc)):
|
|||||||
|
|
||||||
@router.get('/daily', response_model=Daily, tags=['info'])
|
@router.get('/daily', response_model=Daily, tags=['info'])
|
||||||
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||||
return rpc._rpc_daily_profit(timescale, config['stake_currency'],
|
return rpc._rpc_timeunit_profit(timescale, config['stake_currency'],
|
||||||
config.get('fiat_display_currency', ''))
|
config.get('fiat_display_currency', ''))
|
||||||
|
|
||||||
|
|
||||||
|
@ -283,32 +283,47 @@ class RPC:
|
|||||||
columns.append('# Entries')
|
columns.append('# Entries')
|
||||||
return trades_list, columns, fiat_profit_sum
|
return trades_list, columns, fiat_profit_sum
|
||||||
|
|
||||||
def _rpc_daily_profit(
|
def _rpc_timeunit_profit(
|
||||||
self, timescale: int,
|
self, timescale: int,
|
||||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
stake_currency: str, fiat_display_currency: str,
|
||||||
today = datetime.now(timezone.utc).date()
|
timeunit: str = 'days') -> Dict[str, Any]:
|
||||||
profit_days: Dict[date, Dict] = {}
|
"""
|
||||||
|
:param timeunit: Valid entries are 'days', 'weeks', 'months'
|
||||||
|
"""
|
||||||
|
start_date = datetime.now(timezone.utc).date()
|
||||||
|
if timeunit == 'weeks':
|
||||||
|
# weekly
|
||||||
|
start_date = start_date - timedelta(days=start_date.weekday()) # Monday
|
||||||
|
if timeunit == 'months':
|
||||||
|
start_date = start_date.replace(day=1)
|
||||||
|
|
||||||
|
def time_offset(step: int):
|
||||||
|
if timeunit == 'months':
|
||||||
|
return relativedelta(months=step)
|
||||||
|
return timedelta(**{timeunit: step})
|
||||||
|
|
||||||
|
profit_units: 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 day in range(0, timescale):
|
for day in range(0, timescale):
|
||||||
profitday = today - timedelta(days=day)
|
profitday = start_date - time_offset(day)
|
||||||
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 >= profitday,
|
Trade.close_date >= profitday,
|
||||||
Trade.close_date < (profitday + timedelta(days=1))
|
Trade.close_date < (profitday + time_offset(1))
|
||||||
]).order_by(Trade.close_date).all()
|
]).order_by(Trade.close_date).all()
|
||||||
curdayprofit = sum(
|
curdayprofit = sum(
|
||||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||||
profit_days[profitday] = {
|
profit_units[profitday] = {
|
||||||
'amount': curdayprofit,
|
'amount': curdayprofit,
|
||||||
'trades': len(trades)
|
'trades': len(trades)
|
||||||
}
|
}
|
||||||
|
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
'date': key,
|
'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key,
|
||||||
'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'],
|
||||||
@ -317,92 +332,7 @@ class RPC:
|
|||||||
) if self._fiat_converter else 0,
|
) if self._fiat_converter else 0,
|
||||||
'trade_count': value["trades"],
|
'trade_count': value["trades"],
|
||||||
}
|
}
|
||||||
for key, value in profit_days.items()
|
for key, value in profit_units.items()
|
||||||
]
|
|
||||||
return {
|
|
||||||
'stake_currency': stake_currency,
|
|
||||||
'fiat_display_currency': fiat_display_currency,
|
|
||||||
'data': data
|
|
||||||
}
|
|
||||||
|
|
||||||
def _rpc_weekly_profit(
|
|
||||||
self, timescale: int,
|
|
||||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
|
||||||
today = datetime.now(timezone.utc).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 = first_iso_day_of_week - 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]:
|
|
||||||
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):
|
|
||||||
raise RPCException('timescale must be an integer greater than 0')
|
|
||||||
|
|
||||||
for month in range(0, timescale):
|
|
||||||
profitmonth = first_day_of_month - 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': f"{key.year}-{key.month:02d}",
|
|
||||||
'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 {
|
return {
|
||||||
'stake_currency': stake_currency,
|
'stake_currency': stake_currency,
|
||||||
|
@ -6,6 +6,7 @@ This module manage Telegram communication
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from html import escape
|
from html import escape
|
||||||
@ -37,6 +38,15 @@ logger.debug('Included module rpc.telegram ...')
|
|||||||
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TimeunitMappings:
|
||||||
|
header: str
|
||||||
|
message: str
|
||||||
|
message2: str
|
||||||
|
callback: str
|
||||||
|
default: int
|
||||||
|
|
||||||
|
|
||||||
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
Decorator to check if the message comes from the correct chat_id
|
Decorator to check if the message comes from the correct chat_id
|
||||||
@ -563,6 +573,58 @@ class Telegram(RPCHandler):
|
|||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /daily <n>
|
||||||
|
Returns a daily profit (in BTC) over the last n days.
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
vals = {
|
||||||
|
'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7),
|
||||||
|
'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)',
|
||||||
|
'update_weekly', 8),
|
||||||
|
'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6),
|
||||||
|
}
|
||||||
|
val = vals[unit]
|
||||||
|
|
||||||
|
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 val.default
|
||||||
|
except (TypeError, ValueError, IndexError):
|
||||||
|
timescale = val.default
|
||||||
|
try:
|
||||||
|
stats = self._rpc._rpc_timeunit_profit(
|
||||||
|
timescale,
|
||||||
|
stake_cur,
|
||||||
|
fiat_disp_cur,
|
||||||
|
unit
|
||||||
|
)
|
||||||
|
stats_tab = tabulate(
|
||||||
|
[[period['date'],
|
||||||
|
f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}",
|
||||||
|
f"{period['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
||||||
|
f"{period['trade_count']} trades"] for period in stats['data']],
|
||||||
|
headers=[
|
||||||
|
val.header,
|
||||||
|
f'Profit {stake_cur}',
|
||||||
|
f'Profit {fiat_disp_cur}',
|
||||||
|
'Trades',
|
||||||
|
],
|
||||||
|
tablefmt='simple')
|
||||||
|
message = (
|
||||||
|
f'<b>{val.message} Profit over the last {timescale} {val.message2}</b>:\n'
|
||||||
|
f'<pre>{stats_tab}</pre>'
|
||||||
|
)
|
||||||
|
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||||
|
callback_path=val.callback, query=update.callback_query)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _daily(self, update: Update, context: CallbackContext) -> None:
|
def _daily(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -572,35 +634,7 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'days')
|
||||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
|
||||||
try:
|
|
||||||
timescale = int(context.args[0]) if context.args else 7
|
|
||||||
except (TypeError, ValueError, IndexError):
|
|
||||||
timescale = 7
|
|
||||||
try:
|
|
||||||
stats = self._rpc._rpc_daily_profit(
|
|
||||||
timescale,
|
|
||||||
stake_cur,
|
|
||||||
fiat_disp_cur
|
|
||||||
)
|
|
||||||
stats_tab = tabulate(
|
|
||||||
[[day['date'],
|
|
||||||
f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}",
|
|
||||||
f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
|
||||||
f"{day['trade_count']} trades"] for day in stats['data']],
|
|
||||||
headers=[
|
|
||||||
'Day',
|
|
||||||
f'Profit {stake_cur}',
|
|
||||||
f'Profit {fiat_disp_cur}',
|
|
||||||
'Trades',
|
|
||||||
],
|
|
||||||
tablefmt='simple')
|
|
||||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
|
||||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
|
||||||
callback_path="update_daily", query=update.callback_query)
|
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -611,36 +645,7 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'weeks')
|
||||||
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=[
|
|
||||||
'Monday',
|
|
||||||
f'Profit {stake_cur}',
|
|
||||||
f'Profit {fiat_disp_cur}',
|
|
||||||
'Trades',
|
|
||||||
],
|
|
||||||
tablefmt='simple')
|
|
||||||
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,
|
|
||||||
callback_path="update_weekly", query=update.callback_query)
|
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -651,36 +656,7 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'months')
|
||||||
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' \
|
|
||||||
f'</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
|
@authorized_only
|
||||||
def _profit(self, update: Update, context: CallbackContext) -> None:
|
def _profit(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -284,7 +284,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
|||||||
assert isnan(fiat_profit_sum)
|
assert isnan(fiat_profit_sum)
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
def test__rpc_timeunit_profit(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -316,7 +316,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
update.message.text = '/daily 2'
|
update.message.text = '/daily 2'
|
||||||
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency)
|
||||||
assert len(days['data']) == 7
|
assert len(days['data']) == 7
|
||||||
assert days['stake_currency'] == default_conf['stake_currency']
|
assert days['stake_currency'] == default_conf['stake_currency']
|
||||||
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
|
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
|
||||||
@ -332,7 +332,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
||||||
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
rpc._rpc_timeunit_profit(0, stake_currency, fiat_display_currency)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('is_short', [True, False])
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
Loading…
Reference in New Issue
Block a user