Merge pull request #86 from flightcom/feature/advanced-status-command
telegram command: advanced status
This commit is contained in:
commit
0f1d114c03
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user