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
| `/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 (**)
| `/trades [limit]` | | List all recently closed trades in a table format.
| `/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
| `/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:
""" Returns the X last trades """
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:
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]

View File

@ -5,6 +5,7 @@ This module manage Telegram communication
"""
import json
import logging
import arrow
from typing import Any, Callable, Dict
from tabulate import tabulate
@ -92,6 +93,7 @@ class Telegram(RPC):
CommandHandler('stop', self._stop),
CommandHandler('forcesell', self._forcesell),
CommandHandler('forcebuy', self._forcebuy),
CommandHandler('trades', self._trades),
CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
@ -496,6 +498,41 @@ class Telegram(RPC):
except RPCException as 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
def _performance(self, update: Update, context: CallbackContext) -> None:
"""
@ -609,6 +646,7 @@ class Telegram(RPC):
" *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an 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"
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
"regardless of profit`\n"

View File

@ -1089,7 +1089,7 @@ def test_show_trades(mocker, fee, capsys, caplog):
pargs = get_args(args)
pargs['config'] = None
start_show_trades(pargs)
assert log_has("Printing 3 Trades: ", caplog)
assert log_has("Printing 4 Trades: ", caplog)
captured = capsys.readouterr()
assert "Trade(id=1" 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 = 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
trade = Trade(
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'])
assert init_mock.call_count == 1
assert len(trades) == 3
assert len(trades) == 4
assert isinstance(trades, DataFrame)
assert "pair" 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)
trades = rpc._rpc_trade_history(0)
assert len(trades['trades']) == 3
assert trades['trades_count'] == 3
# The first trade is for ETH ... sorting is descending
assert trades['trades'][-1]['pair'] == 'ETH/BTC'
assert trades['trades'][0]['pair'] == 'ETC/BTC'
assert trades['trades'][1]['pair'] == 'ETC/BTC'
assert len(trades['trades']) == 2
assert trades['trades_count'] == 2
# The first closed trade is for ETC ... sorting is descending
assert trades['trades'][-1]['pair'] == 'ETC/BTC'
assert trades['trades'][0]['pair'] == 'XRP/BTC'
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")
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 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):

View File

@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType
from freqtrade.rpc.telegram import Telegram, authorized_only
from freqtrade.state import State
from freqtrade.strategy.interface import SellType
from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange,
patch_get_signal, patch_whitelist)
from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
log_has, patch_exchange, patch_get_signal,
patch_whitelist)
class DummyCls(Telegram):
@ -60,7 +61,7 @@ def test__init__(default_conf, mocker) -> None:
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()
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
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'], "
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
"['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]
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:
msg_mock = MagicMock()
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)
create_mock_trades(fee)
trades = Trade.query.all()
assert len(trades) == 3
assert len(trades) == 4
freqtrade.cancel_all_open_orders()
assert buy_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)
res = Trade.get_overall_performance()
assert len(res) == 1
assert len(res) == 2
assert 'pair' in res[0]
assert 'profit' in res[0]
assert 'count' in res[0]
@ -1004,5 +1004,5 @@ def test_get_best_pair(fee):
create_mock_trades(fee)
res = Trade.get_best_pair()
assert len(res) == 2
assert res[0] == 'ETC/BTC'
assert res[1] == 0.005
assert res[0] == 'XRP/BTC'
assert res[1] == 0.01