Merge pull request #86 from flightcom/feature/advanced-status-command

telegram command: advanced status
This commit is contained in:
Michael Egger 2017-11-05 18:13:25 +01:00 committed by GitHub
commit 0f1d114c03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 161 additions and 5 deletions

View File

@ -19,7 +19,8 @@ Persistence is achieved through sqlite.
### Telegram RPC commands: ### Telegram RPC commands:
* /start: Starts the trader * /start: Starts the trader
* /stop: Stops 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 * /profit: Lists cumulative profit from all finished trades
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`). * /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
* /performance: Show performance of each finished trade grouped by pair * /performance: Show performance of each finished trade grouped by pair

View File

@ -1,6 +1,9 @@
import logging import logging
import re
from datetime import timedelta from datetime import timedelta
from typing import Callable, Any from typing import Callable, Any
from pandas import DataFrame
from tabulate import tabulate
import arrow import arrow
from sqlalchemy import and_, func, text from sqlalchemy import and_, func, text
@ -46,6 +49,7 @@ def init(config: dict) -> None:
CommandHandler('stop', _stop), CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell), CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance), CommandHandler('performance', _performance),
CommandHandler('count', _count),
CommandHandler('help', _help), CommandHandler('help', _help),
] ]
for handle in handles: for handle in handles:
@ -109,6 +113,14 @@ def _status(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :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 # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING: if get_state() != State.RUNNING:
@ -153,6 +165,49 @@ def _status(bot: Bot, update: Update) -> None:
send_msg(message, bot=bot) 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 = "<pre>{}</pre>".format(message)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _profit(bot: Bot, update: Update) -> None: 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) 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 = '<b>Count:</b>\ncurrent/max\n{}/{}\n'.format(len(trades), _CONF['max_open_trades'])
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _help(bot: Bot, update: Update) -> None: def _help(bot: Bot, update: Update) -> None:
""" """
@ -349,16 +424,30 @@ def _help(bot: Bot, update: Update) -> None:
message = """ message = """
*/start:* `Starts the trader` */start:* `Starts the trader`
*/stop:* `Stops 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` */profit:* `Lists cumulative profit from all finished trades`
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit` */forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
*/performance:* `Show performance of each finished trade grouped by pair` */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` */balance:* `Show account balance per currency`
*/help:* `This help message` */help:* `This help message`
""" """
send_msg(message, bot=bot) 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: def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message

View File

@ -1,4 +1,6 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import logging
import re
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock 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.misc import update_state, State, get_state, CONF_SCHEMA
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.telegram import ( 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", "key": "key",
"secret": "secret", "secret": "secret",
"pair_whitelist": [ "pair_whitelist": [
"BTC_ETH" "BTC_ETH",
"BTC_ETC"
] ]
}, },
"telegram": { "telegram": {
@ -107,6 +110,38 @@ def test_status_handle(conf, update, mocker):
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] 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): def test_profit_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
@ -266,6 +301,36 @@ def test_performance_handle(conf, update, mocker):
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0] assert '<code>BTC_ETH\t10.05%</code>' 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): def test_start_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock() msg_mock = MagicMock()

View File

@ -17,7 +17,8 @@ pytest-cov==2.5.1
hyperopt==0.1 hyperopt==0.1
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11 networkx==1.11
tabulate==0.8.1
# Required for plotting data # Required for plotting data
#matplotlib==2.1.0 #matplotlib==2.1.0
#PYQT5==5.9 #PYQT5==5.9