Merge pull request #6615 from cyberjunky/cyber-forcesell-tg
Add selection buttons for trades to forcesell cmd in telegram
This commit is contained in:
		| @@ -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 <trade_id> | ||||
| ### /forceexit <trade_id> | ||||
|  | ||||
| > **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 <pair> [rate] | /forceshort <pair> [rate] | ||||
|  | ||||
|   | ||||
| @@ -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()). | ||||
| @@ -116,6 +115,7 @@ class Telegram(RPCHandler): | ||||
|                                  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'/forceexit$', | ||||
|                                  r'/edge$', r'/health$', r'/help$', r'/version$'] | ||||
|         # Create keys for generation | ||||
|         valid_keys_print = [k.replace('$', '') for k in valid_keys] | ||||
| @@ -197,7 +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_enter_inline), | ||||
|             CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"), | ||||
|             CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"), | ||||
|         ] | ||||
|         for handle in handles: | ||||
|             self._updater.dispatcher.add_handler(handle) | ||||
| @@ -928,23 +929,58 @@ class Telegram(RPCHandler): | ||||
|     @authorized_only | ||||
|     def _force_exit(self, update: Update, context: CallbackContext) -> None: | ||||
|         """ | ||||
|         Handler for /forcesell <id>. | ||||
|         Handler for /forceexit <id>. | ||||
|         Sells the given trade at current price | ||||
|         :param bot: telegram bot | ||||
|         :param update: message update | ||||
|         :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_force_exit(trade_id) | ||||
|             self._send_msg('Force_exit Result: `{result}`'.format(**msg)) | ||||
|         if context.args: | ||||
|             trade_id = context.args[0] | ||||
|             self._force_exit_action(trade_id) | ||||
|         else: | ||||
|             fiat_currency = self._config.get('fiat_display_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((trade[0], f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}")) | ||||
|  | ||||
|         except RPCException as e: | ||||
|             self._send_msg(str(e)) | ||||
|             trade_buttons = [ | ||||
|                 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='force_exit__cancel')]) | ||||
|             self._send_msg(msg="Which trade?", keyboard=buttons_aligned) | ||||
|  | ||||
|     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 _force_exit_inline(self, update: Update, _: CallbackContext) -> None: | ||||
|         if update.callback_query: | ||||
|             query = update.callback_query | ||||
|             if query.data and '__' in query.data: | ||||
|                 # Input data is "force_exit__<tradid|cancel>" | ||||
|                 trade_id = query.data.split("__")[1].split(' ')[0] | ||||
|                 if trade_id == 'cancel': | ||||
|                     query.answer() | ||||
|                     query.edit_message_text(text="Force exit 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': | ||||
| @@ -964,8 +1000,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]]: | ||||
|         return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] | ||||
|  | ||||
|     @authorized_only | ||||
|   | ||||
| @@ -1068,8 +1068,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()) | ||||
| @@ -1212,15 +1212,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._force_exit(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 | ||||
| @@ -1232,6 +1223,59 @@ 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) | ||||
|  | ||||
|     # /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() | ||||
|  | ||||
|     # /forceexit | ||||
|     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 ' | ||||
|     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 exiting - 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) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user