diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f423a9376..250293d25 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -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 ` | | Instantly sells the given trade (Ignoring `minimum_roi`). diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 69faff533..470ed01f6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -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] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13cc1afaf..ab784c962 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -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 + 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"{min(trades['trades_count'], nrecent)} recent trades:\n" + + (f"
{trades_tab}
" 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 |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index ffced956d..3ec7e4798 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index e2bdf7da5..0163eae3b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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', diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index b65db7fd8..718c02f05 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -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 diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index de9327ba9..e5859fcd9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -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, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 355b63f48..f4d7b8ca3 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -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): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 631817624..5f2efdcfe 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -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 "0 recent trades:" in msg_mock.call_args_list[0][0][0] + assert "
" 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:" 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 "
" in msg_mock.call_args_list[0][0][0]
+
+
 def test_help_handle(default_conf, update, mocker) -> None:
     msg_mock = MagicMock()
     mocker.patch.multiple(
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index c7089abfe..fd57eae6f 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -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
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index 8dd27e53a..ab23243a5 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -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