From 46acc8352ff384bc2af0ab606443228d1ee7826e Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Tue, 29 Mar 2022 19:19:07 +0200 Subject: [PATCH 01/15] Add selection buttons for trades to forcesell cmd in telegram --- freqtrade/rpc/telegram.py | 65 +++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5a20520dd..d5721c04e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -190,7 +190,8 @@ class Telegram(RPCHandler): pattern='update_sell_reason_performance'), CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), - CallbackQueryHandler(self._forcebuy_inline), + CallbackQueryHandler(self._forcebuy_inline, pattern="\S+\/\S+"), + CallbackQueryHandler(self._forcesell_inline, pattern="[0-9]+\s\S+\/\S+") ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -379,8 +380,6 @@ class Telegram(RPCHandler): first_avg = filled_orders[0]["safe_price"] for x, order in enumerate(filled_orders): - if order['ft_order_side'] != 'buy': - continue cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_amount = order["amount"] cur_entry_average = order["safe_price"] @@ -446,7 +445,7 @@ class Telegram(RPCHandler): messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() - r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy']) + r['num_entries'] = len(r['filled_entry_orders']) r['sell_reason'] = r.get('sell_reason', "") lines = [ "*Trade ID:* `{trade_id}`" + @@ -490,8 +489,8 @@ class Telegram(RPCHandler): lines.append("*Open Order:* `{open_order}`") lines_detail = self._prepare_entry_details( - r['orders'], r['base_currency'], r['is_open']) - lines.extend(lines_detail if lines_detail else "") + r['filled_entry_orders'], r['base_currency'], r['is_open']) + lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else "")) # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line]).format(**r)) @@ -909,16 +908,44 @@ class Telegram(RPCHandler): :return: None """ - trade_id = context.args[0] if context.args and len(context.args) > 0 else None - if not trade_id: - self._send_msg("You must specify a trade-id or 'all'.") - return - try: - msg = self._rpc._rpc_forcesell(trade_id) - self._send_msg('Forcesell Result: `{result}`'.format(**msg)) + if context.args: + trade_id = context.args[0] + self._forcesell_action(trade_id) + else: + try: + fiat_currency = self._config.get('fiat_display_currency', '') + statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( + self._config['stake_currency'], fiat_currency) - except RPCException as e: - self._send_msg(str(e)) + trades = [] + for trade in statlist: + trades.append(f"{trade[0]} {trade[1]} {trade[3]}") + + trade_buttons = [ + InlineKeyboardButton(text=trade, callback_data=trade) for trade in trades] + buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons) + + buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) + self._send_msg(msg="Which trade?", + keyboard=buttons_aligned) + + except RPCException as e: + self._send_msg(str(e)) + + def _forcesell_action(self, trade_id): + if trade_id != 'cancel': + try: + self._rpc._rpc_forcesell(trade_id) + except RPCException as e: + self._send_msg(str(e)) + + def _forcesell_inline(self, update: Update, _: CallbackContext) -> None: + if update.callback_query: + query = update.callback_query + trade_id = query.data.split(" ")[0] + query.answer() + query.edit_message_text(text=f"Force Selling: {query.data}") + self._forcesell_action(trade_id) def _forcebuy_action(self, pair, price=None): if pair != 'cancel': @@ -940,6 +967,11 @@ class Telegram(RPCHandler): cols=3) -> List[List[InlineKeyboardButton]]: return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] + @staticmethod + def _layout_inline_keyboard_onecol(buttons: List[InlineKeyboardButton], + cols=1) -> List[List[InlineKeyboardButton]]: + return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] + @authorized_only def _forcebuy(self, update: Update, context: CallbackContext) -> None: """ @@ -949,6 +981,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ + if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None @@ -961,7 +994,7 @@ class Telegram(RPCHandler): buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) self._send_msg(msg="Which pair?", - keyboard=buttons_aligned) + keyboard=buttons_aligned) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: From 29d6725fb7bbc1d102a4d9e0309159ad4298952a Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Tue, 29 Mar 2022 19:41:49 +0200 Subject: [PATCH 02/15] Allow forcesell to be a valid keyboard option --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d5721c04e..ac74232aa 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -113,7 +113,7 @@ class Telegram(RPCHandler): r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', - r'/forcebuy$', r'/edge$', r'/health$', r'/help$', r'/version$'] + r'/forcebuy$', r'/forcesell$', r'/edge$', r'/health$', r'/help$', r'/version$'] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -994,7 +994,7 @@ class Telegram(RPCHandler): buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) self._send_msg(msg="Which pair?", - keyboard=buttons_aligned) + keyboard=buttons_aligned) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: From 3ed7f3f2df9f459917a008844ff758d561a90c0d Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Wed, 30 Mar 2022 12:28:30 +0200 Subject: [PATCH 03/15] Display all trade info in buttons First step to fix tests for changed forcesell code --- freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 36 +++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ac74232aa..b2448708a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -919,7 +919,7 @@ class Telegram(RPCHandler): trades = [] for trade in statlist: - trades.append(f"{trade[0]} {trade[1]} {trade[3]}") + trades.append(f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}") trade_buttons = [ InlineKeyboardButton(text=trade, callback_data=trade) for trade in trades] @@ -994,7 +994,7 @@ class Telegram(RPCHandler): buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) self._send_msg(msg="Which pair?", - keyboard=buttons_aligned) + keyboard=buttons_aligned) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f53f48cc2..f99328c0f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1188,15 +1188,6 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] - # No argument - msg_mock.reset_mock() - freqtradebot.state = State.RUNNING - context = MagicMock() - context.args = [] - telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 1 - assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0] - # Invalid argument msg_mock.reset_mock() freqtradebot.state = State.RUNNING @@ -1208,6 +1199,33 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: assert 'invalid argument' in msg_mock.call_args_list[0][0][0] +def test_forcesell_no_tradeid(default_conf, update, mocker) -> None: + + fsell_mock = MagicMock(return_value=None) + mocker.patch('freqtrade.rpc.RPC._rpc_forcesell', fsell_mock) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot) + + context = MagicMock() + context.args = [] + telegram._forcesell(update=update, context=context) + + assert fsell_mock.call_count == 0 + assert msg_mock.call_count == 1 + assert msg_mock.call_args_list[0][1]['msg'] == 'Which trade?' + # assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcesell' + keyboard = msg_mock.call_args_list[0][1]['keyboard'] + # One additional button - cancel + assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 + update = MagicMock() + update.callback_query = MagicMock() + update.callback_query.data = '1 XRP/USDT 1h 2.20% (1.20)' + telegram._forcsell_inline(update, None) + assert fsell_mock.call_count == 1 + + def test_forcebuy_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) From c42af7d09550157e8825e0432b477423ad15cc53 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Wed, 30 Mar 2022 12:41:41 +0200 Subject: [PATCH 04/15] Fixed typo in test file --- tests/rpc/test_rpc_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f99328c0f..39cfbe553 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1222,7 +1222,7 @@ def test_forcesell_no_tradeid(default_conf, update, mocker) -> None: update = MagicMock() update.callback_query = MagicMock() update.callback_query.data = '1 XRP/USDT 1h 2.20% (1.20)' - telegram._forcsell_inline(update, None) + telegram._forcesell_inline(update, None) assert fsell_mock.call_count == 1 From 6c811b3de12d2365fe747b65acaefa81ed52aa1a Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Wed, 30 Mar 2022 19:57:02 +0200 Subject: [PATCH 05/15] Made regex strings raw Removed unwanted changes --- freqtrade/rpc/telegram.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b2448708a..1aab72ba8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -190,8 +190,8 @@ class Telegram(RPCHandler): pattern='update_sell_reason_performance'), CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), - CallbackQueryHandler(self._forcebuy_inline, pattern="\S+\/\S+"), - CallbackQueryHandler(self._forcesell_inline, pattern="[0-9]+\s\S+\/\S+") + CallbackQueryHandler(self._forcebuy_inline, pattern=r"\S+\/\S+"), + CallbackQueryHandler(self._forcesell_inline, pattern=r"[0-9]+\s\S+\/\S+") ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -380,6 +380,8 @@ class Telegram(RPCHandler): first_avg = filled_orders[0]["safe_price"] for x, order in enumerate(filled_orders): + if order['ft_order_side'] != 'buy': + continue cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_amount = order["amount"] cur_entry_average = order["safe_price"] @@ -445,7 +447,7 @@ class Telegram(RPCHandler): messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() - r['num_entries'] = len(r['filled_entry_orders']) + r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy']) r['sell_reason'] = r.get('sell_reason', "") lines = [ "*Trade ID:* `{trade_id}`" + @@ -489,8 +491,8 @@ class Telegram(RPCHandler): lines.append("*Open Order:* `{open_order}`") lines_detail = self._prepare_entry_details( - r['filled_entry_orders'], r['base_currency'], r['is_open']) - lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else "")) + r['orders'], r['base_currency'], r['is_open']) + lines.extend(lines_detail if lines_detail else "") # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line]).format(**r)) From 3d8cfa7ea551148b2439a4d9e17b0661f6190511 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Thu, 31 Mar 2022 08:30:20 +0200 Subject: [PATCH 06/15] Several fixes Code optimizations --- freqtrade/rpc/telegram.py | 28 ++++++++++++---------------- tests/rpc/test_rpc_telegram.py | 27 --------------------------- 2 files changed, 12 insertions(+), 43 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1aab72ba8..84e0e6f50 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -914,25 +914,21 @@ class Telegram(RPCHandler): trade_id = context.args[0] self._forcesell_action(trade_id) else: - try: - fiat_currency = self._config.get('fiat_display_currency', '') - statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( - self._config['stake_currency'], fiat_currency) + fiat_currency = self._config.get('fiat_display_currency', '') + statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( + self._config['stake_currency'], fiat_currency) - trades = [] - for trade in statlist: - trades.append(f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}") + trades = [] + for trade in statlist: + trades.append(f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}") - trade_buttons = [ - InlineKeyboardButton(text=trade, callback_data=trade) for trade in trades] - buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons) + trade_buttons = [ + InlineKeyboardButton(text=trade, callback_data=trade) for trade in trades] + buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons) - buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) - self._send_msg(msg="Which trade?", - keyboard=buttons_aligned) - - except RPCException as e: - self._send_msg(str(e)) + buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) + self._send_msg(msg="Which trade?", + keyboard=buttons_aligned) def _forcesell_action(self, trade_id): if trade_id != 'cancel': diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 39cfbe553..5ce14998e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1199,33 +1199,6 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: assert 'invalid argument' in msg_mock.call_args_list[0][0][0] -def test_forcesell_no_tradeid(default_conf, update, mocker) -> None: - - fsell_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_forcesell', fsell_mock) - - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - - patch_get_signal(freqtradebot) - - context = MagicMock() - context.args = [] - telegram._forcesell(update=update, context=context) - - assert fsell_mock.call_count == 0 - assert msg_mock.call_count == 1 - assert msg_mock.call_args_list[0][1]['msg'] == 'Which trade?' - # assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcesell' - keyboard = msg_mock.call_args_list[0][1]['keyboard'] - # One additional button - cancel - assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 - update = MagicMock() - update.callback_query = MagicMock() - update.callback_query.data = '1 XRP/USDT 1h 2.20% (1.20)' - telegram._forcesell_inline(update, None) - assert fsell_mock.call_count == 1 - - def test_forcebuy_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) From f029702bd18232955df71e16632ce0235e678e03 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Fri, 1 Apr 2022 09:16:35 +0200 Subject: [PATCH 07/15] Fixed flake8 issues --- freqtrade/rpc/telegram.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 84e0e6f50..e1fe8fa20 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -113,7 +113,8 @@ class Telegram(RPCHandler): r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', - r'/forcebuy$', r'/forcesell$', r'/edge$', r'/health$', r'/help$', r'/version$'] + r'/forcebuy$', r'/forcesell$', r'/edge$', r'/health$', r'/help$', + r'/version$'] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -927,9 +928,8 @@ class Telegram(RPCHandler): buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons) buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) - self._send_msg(msg="Which trade?", - keyboard=buttons_aligned) - + self._send_msg(msg="Which trade?", keyboard=buttons_aligned) + def _forcesell_action(self, trade_id): if trade_id != 'cancel': try: @@ -962,12 +962,12 @@ class Telegram(RPCHandler): @staticmethod def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], - cols=3) -> List[List[InlineKeyboardButton]]: + cols=3) -> List[List[InlineKeyboardButton]]: return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @staticmethod def _layout_inline_keyboard_onecol(buttons: List[InlineKeyboardButton], - cols=1) -> List[List[InlineKeyboardButton]]: + cols=1) -> List[List[InlineKeyboardButton]]: return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @authorized_only @@ -991,8 +991,7 @@ class Telegram(RPCHandler): buttons_aligned = self._layout_inline_keyboard(pair_buttons) buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) - self._send_msg(msg="Which pair?", - keyboard=buttons_aligned) + self._send_msg(msg="Which pair?", keyboard=buttons_aligned) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: From 936ada56991333bffbdaa01b0673c6fc12691dc5 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sun, 3 Apr 2022 09:58:55 +0200 Subject: [PATCH 08/15] Fixed syntax error --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 82aff13d2..c1899d4a8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -198,7 +198,7 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._forcebuy_inline, pattern=r"\S+\/\S+"), - CallbackQueryHandler(self._forcesell_inline, pattern=r"[0-9]+\s\S+\/\S+") + CallbackQueryHandler(self._forcesell_inline, pattern=r"[0-9]+\s\S+\/\S+"), CallbackQueryHandler(self._forceenter_inline), ] for handle in handles: From dd61886341b2be2f532f70249a60e626339d58f7 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sun, 3 Apr 2022 12:29:29 +0200 Subject: [PATCH 09/15] Readded missing keyboard commands Rename forcesell methods to forceexit --- freqtrade/rpc/telegram.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c1899d4a8..b335b8a46 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -114,8 +114,8 @@ class Telegram(RPCHandler): r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', - r'/forcebuy$', r'/forcesell$', r'/edge$', r'/health$', r'/help$', - r'/version$'] + r'/forcelong$', r'/forceshort$', r'/forcebuy$', r'/forcesell$', + r'/edge$', r'/health$', r'/help$', r'/version$'] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -197,9 +197,8 @@ class Telegram(RPCHandler): pattern='update_sell_reason_performance'), CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), - CallbackQueryHandler(self._forcebuy_inline, pattern=r"\S+\/\S+"), - CallbackQueryHandler(self._forcesell_inline, pattern=r"[0-9]+\s\S+\/\S+"), - CallbackQueryHandler(self._forceenter_inline), + CallbackQueryHandler(self._forceenter_inline, pattern=r"\S+\/\S+"), + CallbackQueryHandler(self._forceexit_inline, pattern=r"[0-9]+\s\S+\/\S+") ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -943,7 +942,7 @@ class Telegram(RPCHandler): if context.args: trade_id = context.args[0] - self._forcesell_action(trade_id) + self._forceexit_action(trade_id) else: fiat_currency = self._config.get('fiat_display_currency', '') statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( @@ -960,20 +959,20 @@ class Telegram(RPCHandler): buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) self._send_msg(msg="Which trade?", keyboard=buttons_aligned) - def _forcesell_action(self, trade_id): + def _forceexit_action(self, trade_id): if trade_id != 'cancel': try: - self._rpc._rpc_forcesell(trade_id) + self._rpc._rpc_forceexit(trade_id) except RPCException as e: self._send_msg(str(e)) - def _forcesell_inline(self, update: Update, _: CallbackContext) -> None: + def _forceexit_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query trade_id = query.data.split(" ")[0] query.answer() - query.edit_message_text(text=f"Force Selling: {query.data}") - self._forcesell_action(trade_id) + query.edit_message_text(text=f"Manually exiting: {query.data}") + self._forceexit_action(trade_id) def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': From 392967a26f8794e95de49d7ea93ca4131a3b2489 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Apr 2022 18:06:51 +0200 Subject: [PATCH 10/15] Update formatting --- freqtrade/rpc/telegram.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8f11ccbf2..9f86ea580 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -115,9 +115,8 @@ class Telegram(RPCHandler): r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', - r'/forcelong$', r'/forceshort$', r'/forcebuy$', r'/forcesell$', + r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcesell$', r'/edge$', r'/health$', r'/help$', r'/version$'] - # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -989,13 +988,13 @@ class Telegram(RPCHandler): self._force_enter_action(pair, None, order_side) @staticmethod - def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], - cols=3) -> List[List[InlineKeyboardButton]]: + def _layout_inline_keyboard( + buttons: List[InlineKeyboardButton], cols=3) -> List[List[InlineKeyboardButton]]: return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @staticmethod - def _layout_inline_keyboard_onecol(buttons: List[InlineKeyboardButton], - cols=1) -> List[List[InlineKeyboardButton]]: + def _layout_inline_keyboard_onecol( + buttons: List[InlineKeyboardButton], cols=1) -> List[List[InlineKeyboardButton]]: return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @authorized_only From 40eb3f274f25e9edf3e9342e2cf62790bffdefad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Apr 2022 08:36:22 +0200 Subject: [PATCH 11/15] Fix merge mistake --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9f86ea580..fabe718a6 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -958,7 +958,7 @@ class Telegram(RPCHandler): def _forceexit_action(self, trade_id): if trade_id != 'cancel': try: - self._rpc._rpc_forceexit(trade_id) + self._rpc._rpc_force_exit(trade_id) except RPCException as e: self._send_msg(str(e)) From 9cd92ed48ce2217cc4c03a4a59f7afab5eba2619 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Apr 2022 09:24:20 +0200 Subject: [PATCH 12/15] Fix forceexit to work --- freqtrade/rpc/telegram.py | 40 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fabe718a6..c2d050f2b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -197,8 +197,8 @@ class Telegram(RPCHandler): pattern='update_exit_reason_performance'), CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), + CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"), CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"), - CallbackQueryHandler(self._forceexit_inline, pattern=r"[0-9]+\s\S+\/\S+") ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -938,37 +938,49 @@ class Telegram(RPCHandler): if context.args: trade_id = context.args[0] - self._forceexit_action(trade_id) + self._force_exit_action(trade_id) else: fiat_currency = self._config.get('fiat_display_currency', '') - statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( - self._config['stake_currency'], fiat_currency) - + try: + statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( + self._config['stake_currency'], fiat_currency) + except RPCException: + self._send_msg(msg='No open trade found.') + return trades = [] for trade in statlist: - trades.append(f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}") + trades.append((trade[0], f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}")) trade_buttons = [ - InlineKeyboardButton(text=trade, callback_data=trade) for trade in trades] + InlineKeyboardButton(text=trade[1], callback_data=f"force_exit__{trade[0]}") + for trade in trades] buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons) - buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) + buttons_aligned.append([InlineKeyboardButton( + text='Cancel', callback_data='force_exit__cancel')]) self._send_msg(msg="Which trade?", keyboard=buttons_aligned) - def _forceexit_action(self, trade_id): + def _force_exit_action(self, trade_id): if trade_id != 'cancel': try: self._rpc._rpc_force_exit(trade_id) except RPCException as e: self._send_msg(str(e)) - def _forceexit_inline(self, update: Update, _: CallbackContext) -> None: + def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query - trade_id = query.data.split(" ")[0] - query.answer() - query.edit_message_text(text=f"Manually exiting: {query.data}") - self._forceexit_action(trade_id) + if query.data and '__' in query.data: + # Input data is "force_exit__" + trade_id = query.data.split("__")[1].split(' ')[0] + if trade_id == 'cancel': + query.answer() + query.edit_message_text(text="Forcesell canceled") + return + trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first() + query.answer() + query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}") + self._force_exit_action(trade_id) def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': From ddfc68d533a73bd8531b7a98be399f90c4eb1c2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Apr 2022 09:41:01 +0200 Subject: [PATCH 13/15] Add test case for interactive telegram exit --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 52 ++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c2d050f2b..fb86d0481 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -975,7 +975,7 @@ class Telegram(RPCHandler): trade_id = query.data.split("__")[1].split(' ')[0] if trade_id == 'cancel': query.answer() - query.edit_message_text(text="Forcesell canceled") + query.edit_message_text(text="Force exit canceled.") return trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first() query.answer() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 641943620..bb863a004 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1067,8 +1067,8 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, } == last_msg -def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, - ticker_sell_down, mocker) -> None: +def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, + ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -1222,6 +1222,54 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: assert 'invalid argument' in msg_mock.call_args_list[0][0][0] +def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: + default_conf['max_open_trades'] = 4 + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=True), + ) + femock = mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_exit') + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + msg_mock.reset_mock() + + # /forcesell all + context = MagicMock() + context.args = [] + telegram._force_exit(update=update, context=context) + keyboard = msg_mock.call_args_list[0][1]['keyboard'] + # 4 pairs + cancel + assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 + assert keyboard[-1][0].text == "Cancel" + + assert keyboard[1][0].callback_data == 'force_exit__2 L' + update = MagicMock() + update.callback_query = MagicMock() + update.callback_query.data = keyboard[1][0].callback_data + telegram._force_exit_inline(update, None) + assert update.callback_query.answer.call_count == 1 + assert update.callback_query.edit_message_text.call_count == 1 + assert femock.call_count == 1 + assert femock.call_args_list[0][0][0] == '2' + + # Retry selling - but cancel instead + update.callback_query.reset_mock() + telegram._force_exit(update=update, context=context) + # Use cancel button + update.callback_query.data = keyboard[-1][0].callback_data + telegram._force_exit_inline(update, None) + query = update.callback_query + assert query.answer.call_count == 1 + assert query.edit_message_text.call_count == 1 + assert query.edit_message_text.call_args_list[-1][1]['text'] == "Force exit canceled." + + def test_force_enter_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) From f385e2c2b6adaa405d5ae111a324dc0b7ff56bf5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Apr 2022 10:04:10 +0200 Subject: [PATCH 14/15] Update test to also cover "no trade found" scenario --- tests/rpc/test_rpc_telegram.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bb863a004..de777d609 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1235,13 +1235,18 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: patch_get_signal(freqtradebot) + # /forceexit + context = MagicMock() + context.args = [] + telegram._force_exit(update=update, context=context) + # No pair + assert msg_mock.call_args_list[0][1]['msg'] == 'No open trade found.' + # Create some test data freqtradebot.enter_positions() msg_mock.reset_mock() - # /forcesell all - context = MagicMock() - context.args = [] + # /forceexit telegram._force_exit(update=update, context=context) keyboard = msg_mock.call_args_list[0][1]['keyboard'] # 4 pairs + cancel @@ -1258,7 +1263,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: assert femock.call_count == 1 assert femock.call_args_list[0][0][0] == '2' - # Retry selling - but cancel instead + # Retry exiting - but cancel instead update.callback_query.reset_mock() telegram._force_exit(update=update, context=context) # Use cancel button From 282804463c7d55850036d5a411c7b63e2d8b1b4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Apr 2022 09:07:51 +0200 Subject: [PATCH 15/15] Add Documentation for /forceexit without parameter --- docs/telegram-usage.md | 7 +++++-- freqtrade/rpc/telegram.py | 7 +++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index a5709059a..27f5f91b6 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -275,9 +275,12 @@ The relative profit of `1.2%` is the average profit per trade. The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. -### /forcesell +### /forceexit -> **BINANCE:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` +> **BINANCE:** Exiting BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` + +!!! Tip + You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade. ### /forcelong [rate] | /forceshort [rate] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fb86d0481..704dca972 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -103,7 +103,6 @@ class Telegram(RPCHandler): ['/count', '/start', '/stop', '/help'] ] # do not allow commands with mandatory arguments and critical cmds - # like /forcesell and /forcebuy # TODO: DRY! - its not good to list all valid cmds here. But otherwise # this needs refactoring of the whole telegram module (same # problem in _help()). @@ -115,7 +114,8 @@ class Telegram(RPCHandler): r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', - r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcesell$', + r'/forcebuy$', r'/forcelong$', r'/forceshort$', + r'/forcesell$', r'/forceexit$', r'/edge$', r'/health$', r'/help$', r'/version$'] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -929,7 +929,7 @@ class Telegram(RPCHandler): @authorized_only def _force_exit(self, update: Update, context: CallbackContext) -> None: """ - Handler for /forcesell . + Handler for /forceexit . Sells the given trade at current price :param bot: telegram bot :param update: message update @@ -1019,7 +1019,6 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None