Merge pull request #3609 from thopd88/develop

Add telegram /trades command
This commit is contained in:
Matthias 2020-07-25 16:45:09 +02:00 committed by GitHub
commit db8f3a9e9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 106 additions and 22 deletions

View File

@ -56,6 +56,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/show_config` | | Shows part of the current configuration with relevant settings to operation | `/show_config` | | Shows part of the current configuration with relevant settings to operation
| `/status` | | Lists all open trades | `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
| `/trades [limit]` | | List all recently closed trades in a table format.
| `/count` | | Displays number of trades used and available | `/count` | | Displays number of trades used and available
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance | `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).

View File

@ -252,9 +252,10 @@ class RPC:
def _rpc_trade_history(self, limit: int) -> Dict: def _rpc_trade_history(self, limit: int) -> Dict:
""" Returns the X last trades """ """ Returns the X last trades """
if limit > 0: if limit > 0:
trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit) trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.id.desc()).limit(limit)
else: else:
trades = Trade.get_trades().order_by(Trade.id.desc()).all() trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
output = [trade.to_json() for trade in trades] output = [trade.to_json() for trade in trades]

View File

@ -5,6 +5,7 @@ This module manage Telegram communication
""" """
import json import json
import logging import logging
import arrow
from typing import Any, Callable, Dict from typing import Any, Callable, Dict
from tabulate import tabulate from tabulate import tabulate
@ -92,6 +93,7 @@ class Telegram(RPC):
CommandHandler('stop', self._stop), CommandHandler('stop', self._stop),
CommandHandler('forcesell', self._forcesell), CommandHandler('forcesell', self._forcesell),
CommandHandler('forcebuy', self._forcebuy), CommandHandler('forcebuy', self._forcebuy),
CommandHandler('trades', self._trades),
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
@ -496,6 +498,41 @@ class Telegram(RPC):
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /trades <n>
Returns last n recent trades.
:param bot: telegram bot
:param update: message update
:return: None
"""
stake_cur = self._config['stake_currency']
try:
nrecent = int(context.args[0])
except (TypeError, ValueError, IndexError):
nrecent = 10
try:
trades = self._rpc_trade_history(
nrecent
)
trades_tab = tabulate(
[[arrow.get(trade['open_date']).humanize(),
trade['pair'],
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
for trade in trades['trades']],
headers=[
'Open Date',
'Pair',
f'Profit ({stake_cur})',
],
tablefmt='simple')
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@authorized_only @authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None: def _performance(self, update: Update, context: CallbackContext) -> None:
""" """
@ -609,6 +646,7 @@ class Telegram(RPC):
" *table :* `will display trades in a table`\n" " *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n" " `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n" " `pending sell orders are marked with a double asterisk (**)`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit:* `Lists cumulative profit from all finished trades`\n" "*/profit:* `Lists cumulative profit from all finished trades`\n"
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " "*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
"regardless of profit`\n" "regardless of profit`\n"

View File

@ -1089,7 +1089,7 @@ def test_show_trades(mocker, fee, capsys, caplog):
pargs = get_args(args) pargs = get_args(args)
pargs['config'] = None pargs['config'] = None
start_show_trades(pargs) start_show_trades(pargs)
assert log_has("Printing 3 Trades: ", caplog) assert log_has("Printing 4 Trades: ", caplog)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "Trade(id=1" in captured.out assert "Trade(id=1" in captured.out
assert "Trade(id=2" in captured.out assert "Trade(id=2" in captured.out

View File

@ -199,6 +199,20 @@ def create_mock_trades(fee):
) )
Trade.session.add(trade) Trade.session.add(trade)
trade = Trade(
pair='XRP/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.05,
close_rate=0.06,
close_profit=0.01,
exchange='bittrex',
is_open=False,
)
Trade.session.add(trade)
# Simulate prod entry # Simulate prod entry
trade = Trade( trade = Trade(
pair='ETC/BTC', pair='ETC/BTC',

View File

@ -43,7 +43,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
trades = load_trades_from_db(db_url=default_conf['db_url']) trades = load_trades_from_db(db_url=default_conf['db_url'])
assert init_mock.call_count == 1 assert init_mock.call_count == 1
assert len(trades) == 3 assert len(trades) == 4
assert isinstance(trades, DataFrame) assert isinstance(trades, DataFrame)
assert "pair" in trades.columns assert "pair" in trades.columns
assert "open_time" in trades.columns assert "open_time" in trades.columns

View File

@ -284,12 +284,11 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
assert isinstance(trades['trades'][1], dict) assert isinstance(trades['trades'][1], dict)
trades = rpc._rpc_trade_history(0) trades = rpc._rpc_trade_history(0)
assert len(trades['trades']) == 3 assert len(trades['trades']) == 2
assert trades['trades_count'] == 3 assert trades['trades_count'] == 2
# The first trade is for ETH ... sorting is descending # The first closed trade is for ETC ... sorting is descending
assert trades['trades'][-1]['pair'] == 'ETH/BTC' assert trades['trades'][-1]['pair'] == 'ETC/BTC'
assert trades['trades'][0]['pair'] == 'ETC/BTC' assert trades['trades'][0]['pair'] == 'XRP/BTC'
assert trades['trades'][1]['pair'] == 'ETC/BTC'
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,

View File

@ -368,12 +368,12 @@ def test_api_trades(botclient, mocker, ticker, fee, markets):
rc = client_get(client, f"{BASE_URI}/trades") rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc) assert_response(rc)
assert len(rc.json['trades']) == 3
assert rc.json['trades_count'] == 3
rc = client_get(client, f"{BASE_URI}/trades?limit=2")
assert_response(rc)
assert len(rc.json['trades']) == 2 assert len(rc.json['trades']) == 2
assert rc.json['trades_count'] == 2 assert rc.json['trades_count'] == 2
rc = client_get(client, f"{BASE_URI}/trades?limit=1")
assert_response(rc)
assert len(rc.json['trades']) == 1
assert rc.json['trades_count'] == 1
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):

View File

@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType
from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.rpc.telegram import Telegram, authorized_only
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange, from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
patch_get_signal, patch_whitelist) log_has, patch_exchange, patch_get_signal,
patch_whitelist)
class DummyCls(Telegram): class DummyCls(Telegram):
@ -60,7 +61,7 @@ def test__init__(default_conf, mocker) -> None:
assert telegram._config == default_conf assert telegram._config == default_conf
def test_init(default_conf, mocker, caplog) -> None: def test_telegram_init(default_conf, mocker, caplog) -> None:
start_polling = MagicMock() start_polling = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
@ -72,7 +73,7 @@ def test_init(default_conf, mocker, caplog) -> None:
assert start_polling.start_polling.call_count == 1 assert start_polling.start_polling.call_count == 1
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], " "['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], "
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], " "['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
"['edge'], ['help'], ['version']]") "['edge'], ['help'], ['version']]")
@ -1146,6 +1147,36 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0] assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
def test_telegram_trades(mocker, update, default_conf, fee):
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
context = MagicMock()
context.args = []
telegram._trades(update=update, context=context)
assert "<b>0 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
assert "<pre>" not in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
create_mock_trades(fee)
context = MagicMock()
context.args = [5]
telegram._trades(update=update, context=context)
msg_mock.call_count == 1
assert "2 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
assert "Profit (" in msg_mock.call_args_list[0][0][0]
assert "Open Date" in msg_mock.call_args_list[0][0][0]
assert "<pre>" in msg_mock.call_args_list[0][0][0]
def test_help_handle(default_conf, update, mocker) -> None: def test_help_handle(default_conf, update, mocker) -> None:
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(

View File

@ -4094,7 +4094,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee) create_mock_trades(fee)
trades = Trade.query.all() trades = Trade.query.all()
assert len(trades) == 3 assert len(trades) == 4
freqtrade.cancel_all_open_orders() freqtrade.cancel_all_open_orders()
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert sell_mock.call_count == 1 assert sell_mock.call_count == 1

View File

@ -989,7 +989,7 @@ def test_get_overall_performance(fee):
create_mock_trades(fee) create_mock_trades(fee)
res = Trade.get_overall_performance() res = Trade.get_overall_performance()
assert len(res) == 1 assert len(res) == 2
assert 'pair' in res[0] assert 'pair' in res[0]
assert 'profit' in res[0] assert 'profit' in res[0]
assert 'count' in res[0] assert 'count' in res[0]
@ -1004,5 +1004,5 @@ def test_get_best_pair(fee):
create_mock_trades(fee) create_mock_trades(fee)
res = Trade.get_best_pair() res = Trade.get_best_pair()
assert len(res) == 2 assert len(res) == 2
assert res[0] == 'ETC/BTC' assert res[0] == 'XRP/BTC'
assert res[1] == 0.005 assert res[1] == 0.01