From 271e4500d984145a07de6c6de58f267a8b0cb904 Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Wed, 31 Mar 2021 18:08:53 -0300 Subject: [PATCH 1/7] telegram: Adding dynamic keyboard to /forcebuy response --- freqtrade/rpc/telegram.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 92899d67f..d263cb605 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -626,6 +626,10 @@ class Telegram(RPCHandler): self._rpc._rpc_forcebuy(pair, price) except RPCException as e: self._send_msg(str(e)) + else: + whitelist = self._rpc._rpc_whitelist()['whitelist'] + pairs_keyboard: List[List[Union[str, KeyboardButton]]] = [[f'/forcebuy {pair}' for pair in whitelist]] + self._send_msg("Which pair?", keyboard=pairs_keyboard) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -942,7 +946,8 @@ class Telegram(RPCHandler): ) def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False) -> None: + disable_notification: bool = False, + keyboard: List[List[Union[str, KeyboardButton]]] = None) -> None: """ Send given markdown message :param msg: message @@ -950,7 +955,9 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ - reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) + if keyboard is None: + keyboard = self._keyboard + reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( From e2f28991e6e5f3b7d429a3d50ad63ba6dd7f47c5 Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Wed, 31 Mar 2021 21:51:15 -0300 Subject: [PATCH 2/7] telegram: Wrapping long line --- freqtrade/rpc/telegram.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d263cb605..ae308a1cf 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -628,7 +628,9 @@ class Telegram(RPCHandler): self._send_msg(str(e)) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] - pairs_keyboard: List[List[Union[str, KeyboardButton]]] = [[f'/forcebuy {pair}' for pair in whitelist]] + pairs_keyboard: List[List[Union[str, KeyboardButton]]] = [ + [f'/forcebuy {pair}' for pair in whitelist] + ] self._send_msg("Which pair?", keyboard=pairs_keyboard) @authorized_only From 50bdae8eb200294136d03f0c287847aaf9b5386b Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Sat, 10 Apr 2021 20:13:04 -0300 Subject: [PATCH 3/7] telegram: Adding forcebuy inline keyboard --- freqtrade/rpc/telegram.py | 62 +++++++++++++++++++++++++++------- tests/rpc/test_rpc_telegram.py | 28 +++++++++++++++ 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ae308a1cf..b24638e48 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,9 +12,9 @@ from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update +from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update, BotCommand, InlineKeyboardMarkup, InlineKeyboardButton from telegram.error import NetworkError, TelegramError -from telegram.ext import CallbackContext, CommandHandler, Updater +from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ @@ -169,6 +169,11 @@ class Telegram(RPCHandler): [h.command for h in handles] ) + self._current_callback_query_handler = None + self._callback_query_handlers = { + 'forcebuy': CallbackQueryHandler(self._forcebuy_inline) + } + def cleanup(self) -> None: """ Stops all running telegram threads. @@ -610,6 +615,24 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) + def _forcebuy_action(self, pair, price = None): + try: + self._rpc._rpc_forcebuy(pair, price) + except RPCException as e: + self._send_msg(str(e)) + + def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None: + query = update.callback_query + pair = query.data + query.answer() + query.edit_message_text(text=f"Force Buying: {pair}") + self._forcebuy_action(pair) + + @staticmethod + 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)] + @authorized_only def _forcebuy(self, update: Update, context: CallbackContext) -> None: """ @@ -622,16 +645,13 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None - try: - self._rpc._rpc_forcebuy(pair, price) - except RPCException as e: - self._send_msg(str(e)) + self._forcebuy_action(pair, price) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] - pairs_keyboard: List[List[Union[str, KeyboardButton]]] = [ - [f'/forcebuy {pair}' for pair in whitelist] - ] - self._send_msg("Which pair?", keyboard=pairs_keyboard) + pairs = [InlineKeyboardButton(pair, callback_data=pair) for pair in whitelist] + self._send_inline_msg("Which pair?", + keyboard=self._layout_inline_keyboard(pairs), + callback_query_handler='forcebuy') @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -947,9 +967,27 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) + def _send_inline_msg(self, msg: str, callback_query_handler, + parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, + keyboard: List[List[InlineKeyboardButton]] = None, ) -> None: + """ + Send given markdown message + :param msg: message + :param bot: alternative bot + :param parse_mode: telegram parse mode + :return: None + """ + if self._current_callback_query_handler: + self._updater.dispatcher.remove_handler(self._current_callback_query_handler) + self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler] + self._updater.dispatcher.add_handler(self._current_callback_query_handler) + + self._send_msg(msg, parse_mode, disable_notification, keyboard, reply_markup=InlineKeyboardMarkup) + def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[Union[str, KeyboardButton]]] = None) -> None: + keyboard: List[List[Union[str, KeyboardButton]]] = None, + reply_markup=ReplyKeyboardMarkup) -> None: """ Send given markdown message :param msg: message @@ -959,7 +997,7 @@ class Telegram(RPCHandler): """ if keyboard is None: keyboard = self._keyboard - reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) + reply_markup = reply_markup(keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 27babb1b7..7fdbf77f7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -6,6 +6,7 @@ import re from datetime import datetime from random import choice, randint from string import ascii_uppercase +from functools import reduce from unittest.mock import ANY, MagicMock import arrow @@ -53,6 +54,12 @@ class DummyCls(Telegram): """ raise Exception('test') +def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=None): + inline_msg_mock = MagicMock() + telegram, ftbot, msg_mock = get_telegram_testobject(mocker, default_conf) + mocker.patch('freqtrade.rpc.telegram.Telegram._send_inline_msg', inline_msg_mock) + + return telegram, ftbot, msg_mock, inline_msg_mock def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): msg_mock = MagicMock() @@ -901,6 +908,27 @@ def test_forcebuy_handle_exception(default_conf, update, mocker) -> None: assert msg_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' +def test_forcebuy_no_pair(default_conf, update, mocker) -> None: + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + + fbuy_mock = MagicMock(return_value=None) + mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) + + telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + + context = MagicMock() + context.args = [] + telegram._forcebuy(update=update, context=context) + + assert fbuy_mock.call_count == 0 + assert inline_msg_mock.call_count == 1 + assert inline_msg_mock.call_args_list[0][0][0] == 'Which pair?' + assert inline_msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' + keyboard = inline_msg_mock.call_args_list[0][1]['keyboard'] + assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 + + def test_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None: From 5fae4ea2fdcc46f31117b7d64dd1b477e7b8ffe3 Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Wed, 14 Apr 2021 17:52:47 -0300 Subject: [PATCH 4/7] telegram: Formatting code --- freqtrade/rpc/telegram.py | 12 ++++++++---- tests/rpc/test_rpc_telegram.py | 5 ++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b24638e48..52cd7a5ed 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,7 +12,10 @@ from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update, BotCommand, InlineKeyboardMarkup, InlineKeyboardButton +from telegram import ( + KeyboardButton, ParseMode, ReplyKeyboardMarkup, + Update, InlineKeyboardMarkup, InlineKeyboardButton + ) from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler from telegram.utils.helpers import escape_markdown @@ -615,7 +618,7 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) - def _forcebuy_action(self, pair, price = None): + def _forcebuy_action(self, pair, price=None): try: self._rpc._rpc_forcebuy(pair, price) except RPCException as e: @@ -969,7 +972,7 @@ class Telegram(RPCHandler): def _send_inline_msg(self, msg: str, callback_query_handler, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[InlineKeyboardButton]] = None, ) -> None: + keyboard: List[List[InlineKeyboardButton]] = None, ) -> None: """ Send given markdown message :param msg: message @@ -982,7 +985,8 @@ class Telegram(RPCHandler): self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler] self._updater.dispatcher.add_handler(self._current_callback_query_handler) - self._send_msg(msg, parse_mode, disable_notification, keyboard, reply_markup=InlineKeyboardMarkup) + self._send_msg(msg, parse_mode, disable_notification, keyboard, + reply_markup=InlineKeyboardMarkup) def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7fdbf77f7..c80709ac1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -54,6 +54,7 @@ class DummyCls(Telegram): """ raise Exception('test') + def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=None): inline_msg_mock = MagicMock() telegram, ftbot, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -61,6 +62,7 @@ def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=N return telegram, ftbot, msg_mock, inline_msg_mock + def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): msg_mock = MagicMock() if mock: @@ -914,7 +916,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: fbuy_mock = MagicMock(return_value=None) mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) - telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker, default_conf) + telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker, + default_conf) patch_get_signal(freqtradebot, (True, False)) context = MagicMock() From e3c5a4b3fc8b1e33eca8c3313d42889e20bf8b0b Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Wed, 14 Apr 2021 18:20:10 -0300 Subject: [PATCH 5/7] telegram: Formatting imports --- freqtrade/rpc/telegram.py | 8 +++----- tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 52cd7a5ed..8a25fc61f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,12 +12,10 @@ from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import ( - KeyboardButton, ParseMode, ReplyKeyboardMarkup, - Update, InlineKeyboardMarkup, InlineKeyboardButton - ) +from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode, + ReplyKeyboardMarkup, Update) from telegram.error import NetworkError, TelegramError -from telegram.ext import CallbackContext, CommandHandler, Updater, CallbackQueryHandler +from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index c80709ac1..a3147f956 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -4,9 +4,9 @@ import re from datetime import datetime +from functools import reduce from random import choice, randint from string import ascii_uppercase -from functools import reduce from unittest.mock import ANY, MagicMock import arrow From 7a98de10ea6cea4c13e74fc3c1cc9ae36817cf6c Mon Sep 17 00:00:00 2001 From: Gonzalo Matheu Date: Thu, 15 Apr 2021 09:31:20 -0300 Subject: [PATCH 6/7] telegram: Formatting typings --- freqtrade/rpc/telegram.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8a25fc61f..0d978cc6e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -8,7 +8,7 @@ import logging from datetime import timedelta from html import escape from itertools import chain -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List, Optional, Union, cast import arrow from tabulate import tabulate @@ -88,7 +88,7 @@ class Telegram(RPCHandler): Validates the keyboard configuration from telegram config section. """ - self._keyboard: List[List[Union[str, KeyboardButton]]] = [ + self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [ ['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help'] @@ -170,7 +170,7 @@ class Telegram(RPCHandler): [h.command for h in handles] ) - self._current_callback_query_handler = None + self._current_callback_query_handler: Optional[CallbackQueryHandler] = None self._callback_query_handlers = { 'forcebuy': CallbackQueryHandler(self._forcebuy_inline) } @@ -623,11 +623,12 @@ class Telegram(RPCHandler): self._send_msg(str(e)) def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None: - query = update.callback_query - pair = query.data - query.answer() - query.edit_message_text(text=f"Force Buying: {pair}") - self._forcebuy_action(pair) + if update.callback_query: + query = update.callback_query + pair = query.data + query.answer() + query.edit_message_text(text=f"Force Buying: {pair}") + self._forcebuy_action(pair) @staticmethod def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], @@ -983,12 +984,13 @@ class Telegram(RPCHandler): self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler] self._updater.dispatcher.add_handler(self._current_callback_query_handler) - self._send_msg(msg, parse_mode, disable_notification, keyboard, + self._send_msg(msg, parse_mode, disable_notification, + cast(List[List[Union[str, KeyboardButton, InlineKeyboardButton]]], keyboard), reply_markup=InlineKeyboardMarkup) def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[Union[str, KeyboardButton]]] = None, + keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None, reply_markup=ReplyKeyboardMarkup) -> None: """ Send given markdown message From 77302ea178896aaba9f2608557a4c6bbb796e7c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 16:01:49 +0200 Subject: [PATCH 7/7] Update documentation for forcebuy query --- docs/assets/telegram_forcebuy.png | Bin 0 -> 17895 bytes docs/telegram-usage.md | 9 +++++++-- tests/rpc/test_rpc_telegram.py | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 docs/assets/telegram_forcebuy.png diff --git a/docs/assets/telegram_forcebuy.png b/docs/assets/telegram_forcebuy.png new file mode 100644 index 0000000000000000000000000000000000000000..b0592bff3571dab298a1f951f1aaa8eae255a011 GIT binary patch literal 17895 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 **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` -### /forcebuy +### /forcebuy [rate] > **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) +Omitting the pair will open a query asking for the pair to buy (based on the current whitelist). + +![Telegram force-buy screenshot](assets/telegram_forcebuy.png) + Note that for this to work, `forcebuy_enable` needs to be set to true. [More details](configuration.md#understand-forcebuy_enable) @@ -254,7 +258,8 @@ Note that for this to work, `forcebuy_enable` needs to be set to true. ### /performance Return the performance of each crypto-currency the bot has sold. -> Performance: + +> Performance: > 1. `RCN/BTC 57.77%` > 2. `PAY/BTC 56.91%` > 3. `VIB/BTC 47.07%` diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a3147f956..6e818ff8d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -930,6 +930,11 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: assert inline_msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' keyboard = inline_msg_mock.call_args_list[0][1]['keyboard'] assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 + update = MagicMock() + update.callback_query = MagicMock() + update.callback_query.data = 'XRP/USDT' + telegram._forcebuy_inline(update, None) + assert fbuy_mock.call_count == 1 def test_performance_handle(default_conf, update, ticker, fee,