From ccb8c3c3524b62ce48a4b7431d2ffdec032841fc Mon Sep 17 00:00:00 2001 From: Stephen Dade Date: Sun, 10 Dec 2017 17:32:40 +1100 Subject: [PATCH 1/2] Added daily profit telegram command --- README.md | 1 + freqtrade/rpc/telegram.py | 42 ++++++++++++++++++++++++++-- freqtrade/tests/test_rpc_telegram.py | 37 ++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 28cba0855..af4fdc887 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Persistence is achieved through sqlite. * /forcesell |all: Instantly sells the given trade (Ignoring `minimum_roi`). * /performance: Show performance of each finished trade grouped by pair * /balance: Show account balance per currency +* /daily [n]: Shows profit or loss per day, over the last n (default 7) days * /help: Show help message * /version: Show version diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e2b472119..0ef55efaf 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,11 +1,11 @@ import logging import re -from datetime import timedelta +from datetime import timedelta, date from typing import Callable, Any import arrow from pandas import DataFrame -from sqlalchemy import and_, func, text +from sqlalchemy import and_, func, text, between from tabulate import tabulate from telegram import ParseMode, Bot, Update, ReplyKeyboardMarkup from telegram.error import NetworkError, TelegramError @@ -49,6 +49,7 @@ def init(config: dict) -> None: CommandHandler('stop', _stop), CommandHandler('forcesell', _forcesell), CommandHandler('performance', _performance), + CommandHandler('daily', _daily), CommandHandler('count', _count), CommandHandler('help', _help), CommandHandler('version', _version), @@ -206,7 +207,44 @@ def _status_table(bot: Bot, update: Update) -> None: send_msg(message, parse_mode=ParseMode.HTML) +@authorized_only +def _daily(bot: Bot, update: Update) -> None: + """ + Handler for /daily + Returns a daily profit (in BTC) over the last n days. + Default is 7 days + :param bot: telegram bot + :param update: message update + :return: None + """ + trades = Trade.query.order_by(Trade.close_date).all() + today = date.today().toordinal() + profit_days = {} + + try: + timescale = update.message.text.replace('/daily', '').strip() + except: + timescale = 7 + for day in range(0, timescale): + #need to query between day+1 and day-1 + nextdate = date.fromordinal(today-day+1) + prevdate = date.fromordinal(today-day-1) + trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() + curdayprofit = 0 + for trade in trades: + curdayprofit += trade.close_profit * trade.stake_amount + profit_days[date.fromordinal(today-day)] = curdayprofit + + + stats = '\n'.join('{index}\t{profit} BTC'.format( + index=key, + profit=value, + ) for key, value in profit_days.items()) + + message = 'Daily Profit:\n{}'.format(stats) + send_msg(message, bot=bot) + @authorized_only def _profit(bot: Bot, update: Update) -> None: """ diff --git a/freqtrade/tests/test_rpc_telegram.py b/freqtrade/tests/test_rpc_telegram.py index d8cb43966..359810b8a 100644 --- a/freqtrade/tests/test_rpc_telegram.py +++ b/freqtrade/tests/test_rpc_telegram.py @@ -1,6 +1,6 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 import re -from datetime import datetime +from datetime import datetime, date from random import randint from unittest.mock import MagicMock @@ -14,7 +14,7 @@ from freqtrade.misc import update_state, State, get_state from freqtrade.persistence import Trade from freqtrade.rpc import telegram from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \ - _profit, _forcesell, _performance, _count, _start, _stop, _balance, _version, _help + _profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help def test_is_enabled(default_conf, mocker): @@ -316,7 +316,40 @@ def test_performance_handle( assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'BTC_ETH\t10.05%' in msg_mock.call_args_list[0][0][0] +def test_daily_handle( + default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + msg_mock = MagicMock() + mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) + mocker.patch.multiple('freqtrade.rpc.telegram', + _CONF=default_conf, + init=MagicMock(), + send_msg=msg_mock) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker) + init(default_conf, create_engine('sqlite://')) + # Create some test data + create_trade(15.0) + 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 + + _daily(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'Daily' in msg_mock.call_args_list[0][0][0] + assert str(date.today()) + '\t1.50701325 BTC' in msg_mock.call_args_list[0][0][0] + def test_count_handle(default_conf, update, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) From 0b18c93d19d52fc6a41f7e1e508058bde7d138c7 Mon Sep 17 00:00:00 2001 From: Stephen Dade Date: Tue, 12 Dec 2017 19:38:18 +1100 Subject: [PATCH 2/2] Daily profit command - better message formatting and minor fixes --- README.md | 2 +- freqtrade/rpc/telegram.py | 42 +++++++++++++++------------- freqtrade/tests/test_rpc_telegram.py | 15 ++++++++-- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index af4fdc887..a372c5a93 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Persistence is achieved through sqlite. * /forcesell |all: Instantly sells the given trade (Ignoring `minimum_roi`). * /performance: Show performance of each finished trade grouped by pair * /balance: Show account balance per currency -* /daily [n]: Shows profit or loss per day, over the last n (default 7) days +* /daily : Shows profit or loss per day, over the last n days * /help: Show help message * /version: Show version diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0ef55efaf..ab288854a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -212,7 +212,6 @@ def _daily(bot: Bot, update: Update) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. - Default is 7 days :param bot: telegram bot :param update: message update :return: None @@ -222,28 +221,32 @@ def _daily(bot: Bot, update: Update) -> None: profit_days = {} try: - timescale = update.message.text.replace('/daily', '').strip() + timescale = int(update.message.text.replace('/daily', '').strip()) except: - timescale = 7 + send_msg('*Daily :* `must be an integer greater than 0`', bot=bot) + return + + if not (isinstance(timescale, int) and timescale > 0): + send_msg('*Daily :* `must be an integer greater than 0`', bot=bot) + return for day in range(0, timescale): - #need to query between day+1 and day-1 - nextdate = date.fromordinal(today-day+1) - prevdate = date.fromordinal(today-day-1) - trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() - curdayprofit = 0 - for trade in trades: - curdayprofit += trade.close_profit * trade.stake_amount - profit_days[date.fromordinal(today-day)] = curdayprofit + #need to query between day+1 and day-1 + nextdate = date.fromordinal(today-day+1) + prevdate = date.fromordinal(today-day-1) + trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() + curdayprofit = 0 + for trade in trades: + curdayprofit += trade.close_profit * trade.stake_amount + profit_days[date.fromordinal(today-day)] = curdayprofit + stats = [] + for key, value in profit_days.items(): + stats.append([key, str(value) + ' BTC']) + stats = tabulate(stats, headers=['Day', 'Profit'], tablefmt='simple') - stats = '\n'.join('{index}\t{profit} BTC'.format( - index=key, - profit=value, - ) for key, value in profit_days.items()) - - message = 'Daily Profit:\n{}'.format(stats) - send_msg(message, bot=bot) + message = 'Daily Profit over the last {} days:\n
{}
'.format(timescale, stats) + send_msg(message, bot=bot, parse_mode=ParseMode.HTML) @authorized_only def _profit(bot: Bot, update: Update) -> None: @@ -469,6 +472,7 @@ def _help(bot: Bot, update: Update) -> None: */profit:* `Lists cumulative profit from all finished trades` */forcesell |all:* `Instantly sells the given trade or all trades, regardless of profit` */performance:* `Show performance of each finished trade grouped by pair` +*/daily :* `Shows profit or loss per day, over the last n days` */count:* `Show number of trades running compared to allowed number of trades` */balance:* `Show account balance per currency` */help:* `This help message` @@ -514,7 +518,7 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO bot = bot or _UPDATER.bot - keyboard = [['/status table', '/profit', '/performance', ], + keyboard = [['/daily', '/status table', '/profit', '/performance', ], ['/balance', '/status', '/count'], ['/start', '/stop', '/help']] diff --git a/freqtrade/tests/test_rpc_telegram.py b/freqtrade/tests/test_rpc_telegram.py index 359810b8a..78f5f42c0 100644 --- a/freqtrade/tests/test_rpc_telegram.py +++ b/freqtrade/tests/test_rpc_telegram.py @@ -330,7 +330,7 @@ def test_daily_handle( validate_pairs=MagicMock(), get_ticker=ticker) init(default_conf, create_engine('sqlite://')) - + # Create some test data create_trade(15.0) trade = Trade.query.first() @@ -345,10 +345,21 @@ def test_daily_handle( trade.close_date = datetime.utcnow() trade.is_open = False + #try valid data + update.message.text = '/daily 7' _daily(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'Daily' in msg_mock.call_args_list[0][0][0] - assert str(date.today()) + '\t1.50701325 BTC' in msg_mock.call_args_list[0][0][0] + assert str(date.today()) + ' 1.50701325 BTC' in msg_mock.call_args_list[0][0][0] + + #try invalid data + msg_mock.reset_mock() + update_state(State.RUNNING) + update.message.text = '/daily' + _daily(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] + def test_count_handle(default_conf, update, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf)