diff --git a/README.md b/README.md index fa45b79e0..ee738cb28 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ Persistence is achieved through sqlite. ### Telegram RPC commands: * /start: Starts the trader * /stop: Stops the trader -* /status: Lists all open trades +* /status [table]: Lists all open trades +* /count: Displays number of open trades * /profit: Lists cumulative profit from all finished trades * /forcesell : Instantly sells the given trade (Ignoring `minimum_roi`). * /performance: Show performance of each finished trade grouped by pair diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9f2559000..ca565d16d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,6 +1,9 @@ import logging +import re from datetime import timedelta from typing import Callable, Any +from pandas import DataFrame +from tabulate import tabulate import arrow from sqlalchemy import and_, func, text @@ -46,6 +49,7 @@ def init(config: dict) -> None: CommandHandler('stop', _stop), CommandHandler('forcesell', _forcesell), CommandHandler('performance', _performance), + CommandHandler('count', _count), CommandHandler('help', _help), ] for handle in handles: @@ -109,6 +113,14 @@ def _status(bot: Bot, update: Update) -> None: :param update: message update :return: None """ + + # Check if additional parameters are passed + params = update.message.text.replace('/status', '').split(' ') \ + if update.message.text else [] + if 'table' in params: + _status_table(bot, update) + return + # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() if get_state() != State.RUNNING: @@ -153,6 +165,49 @@ def _status(bot: Bot, update: Update) -> None: send_msg(message, bot=bot) +@authorized_only +def _status_table(bot: Bot, update: Update) -> None: + """ + Handler for /status table. + Returns the current TradeThread status in table format + :param bot: telegram bot + :param update: message update + :return: None + """ + # Fetch open trade + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + if get_state() != State.RUNNING: + send_msg('*Status:* `trader is not running`', bot=bot) + elif not trades: + send_msg('*Status:* `no active order`', bot=bot) + else: + trades_list = [] + for trade in trades: + # calculate profit and send message to user + current_rate = exchange.get_ticker(trade.pair)['bid'] + current_profit = '{:.2f}'.format(100 * ((current_rate \ + - trade.open_rate) / trade.open_rate)) + + row = [ + trade.id, + trade.pair, + shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), + current_profit + ] + + trades_list.append(row) + + columns = ['ID', 'Pair', 'Since', 'Profit'] + + df_statuses = DataFrame.from_records(trades_list, columns=columns) + df_statuses = df_statuses.set_index(columns[0]) + + message = tabulate(df_statuses, headers='keys', tablefmt='simple') + message = "
{}
".format(message) + + send_msg(message, parse_mode=ParseMode.HTML) + + @authorized_only def _profit(bot: Bot, update: Update) -> None: """ @@ -337,6 +392,26 @@ def _performance(bot: Bot, update: Update) -> None: send_msg(message, parse_mode=ParseMode.HTML) +@authorized_only +def _count(bot: Bot, update: Update) -> None: + """ + Handler for /count. + Returns the number of trades running + :param bot: telegram bot + :param update: message update + :return: None + """ + if get_state() != State.RUNNING: + send_msg('`trader is not running`', bot=bot) + return + + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + message = 'Count:\ncurrent/max\n{}/{}\n'.format(len(trades), _CONF['max_open_trades']) + + logger.debug(message) + send_msg(message, parse_mode=ParseMode.HTML) + + @authorized_only def _help(bot: Bot, update: Update) -> None: """ @@ -349,16 +424,30 @@ def _help(bot: Bot, update: Update) -> None: message = """ */start:* `Starts the trader` */stop:* `Stops the trader` -*/status:* `Lists all open trades` +*/status [table]:* `Lists all open trades` + *table :* `will display trades in a table` */profit:* `Lists cumulative profit from all finished trades` */forcesell :* `Instantly sells the given trade, regardless of profit` */performance:* `Show performance of each finished trade grouped by pair` +*/count:* `Show number of trades running compared to allowed number of trades` */balance:* `Show account balance per currency` */help:* `This help message` """ send_msg(message, bot=bot) +def shorten_date(date): + """ + Trim the date so it fits on small screens + """ + new_date = re.sub('seconds?', 'sec', date) + new_date = re.sub('minutes?', 'min', new_date) + new_date = re.sub('hours?', 'h', new_date) + new_date = re.sub('days?', 'd', new_date) + new_date = re.sub('^an?', '1', new_date) + return new_date + + def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: """ Send given markdown message diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 8ee830d92..9e45aa39d 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -1,4 +1,6 @@ # pragma pylint: disable=missing-docstring +import logging +import re from datetime import datetime from unittest.mock import MagicMock @@ -10,7 +12,7 @@ from freqtrade.main import init, create_trade from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA from freqtrade.persistence import Trade from freqtrade.rpc.telegram import ( - _status, _profit, _forcesell, _performance, _start, _stop, _balance + _status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance ) @@ -35,7 +37,8 @@ def conf(): "key": "key", "secret": "secret", "pair_whitelist": [ - "BTC_ETH" + "BTC_ETH", + "BTC_ETC" ] }, "telegram": { @@ -107,6 +110,38 @@ def test_status_handle(conf, update, mocker): assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] +def test_status_table_handle(conf, update, mocker): + mocker.patch.dict('freqtrade.main._CONF', conf) + mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) + msg_mock = MagicMock() + mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, + 'last': 0.07256061 + }), + buy=MagicMock(return_value='mocked_order_id')) + init(conf, 'sqlite://') + + # Create some test data + trade = create_trade(15.0) + assert trade + Trade.session.add(trade) + Trade.session.flush() + + _status_table(bot=MagicBot(), update=update) + + text = re.sub('<\/?pre>', '', msg_mock.call_args_list[-1][0][0]) + line = text.split("\n") + fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ') + + assert int(fields[0]) == 1 + assert fields[1] == 'BTC_ETH' + assert msg_mock.call_count == 2 + + def test_profit_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) @@ -266,6 +301,36 @@ def test_performance_handle(conf, update, mocker): assert 'BTC_ETH\t10.05%' in msg_mock.call_args_list[-1][0][0] + +def test_count_handle(conf, update, mocker): + mocker.patch.dict('freqtrade.main._CONF', conf) + mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) + msg_mock = MagicMock() + mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, + 'last': 0.07256061 + }), + buy=MagicMock(return_value='mocked_order_id')) + init(conf, 'sqlite://') + + # Create some test data + trade = create_trade(15.0) + trade2 = create_trade(15.0) + assert trade + assert trade2 + Trade.session.add(trade) + Trade.session.add(trade2) + Trade.session.flush() + + _count(bot=MagicBot(), update=update) + line = msg_mock.call_args_list[-1][0][0].split("\n") + assert line[2] == '{}/{}'.format(2, conf['max_open_trades']) + + def test_start_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) msg_mock = MagicMock() diff --git a/requirements.txt b/requirements.txt index 69379b025..83292154e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,8 @@ pytest-cov==2.5.1 hyperopt==0.1 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 networkx==1.11 +tabulate==0.8.1 # Required for plotting data #matplotlib==2.1.0 -#PYQT5==5.9 \ No newline at end of file +#PYQT5==5.9