Merge pull request #4746 from gmatheu/contribution/telegram_forcebuy_inline_keyboard

Telegram: forcebuy inline keyboard
This commit is contained in:
Matthias 2021-05-23 15:32:47 +01:00 committed by GitHub
commit ae037b0ec1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 18 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -250,10 +250,14 @@ Return a summary of your profit/loss and performance.
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` > **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
### /forcebuy <pair> ### /forcebuy <pair> [rate]
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) > **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. Note that for this to work, `forcebuy_enable` needs to be set to true.
[More details](configuration.md#understand-forcebuy_enable) [More details](configuration.md#understand-forcebuy_enable)

View File

@ -9,13 +9,14 @@ from datetime import timedelta
from html import escape from html import escape
from itertools import chain from itertools import chain
from math import isnan from math import isnan
from typing import Any, Callable, Dict, List, Union from typing import Any, Callable, Dict, List, Optional, Union, cast
import arrow import arrow
from tabulate import tabulate from tabulate import tabulate
from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode,
ReplyKeyboardMarkup, Update)
from telegram.error import NetworkError, TelegramError from telegram.error import NetworkError, TelegramError
from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown from telegram.utils.helpers import escape_markdown
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
@ -88,7 +89,7 @@ class Telegram(RPCHandler):
Validates the keyboard configuration from telegram config Validates the keyboard configuration from telegram config
section. section.
""" """
self._keyboard: List[List[Union[str, KeyboardButton]]] = [ self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [
['/daily', '/profit', '/balance'], ['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'], ['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help'] ['/count', '/start', '/stop', '/help']
@ -170,6 +171,11 @@ class Telegram(RPCHandler):
[h.command for h in handles] [h.command for h in handles]
) )
self._current_callback_query_handler: Optional[CallbackQueryHandler] = None
self._callback_query_handlers = {
'forcebuy': CallbackQueryHandler(self._forcebuy_inline)
}
def cleanup(self) -> None: def cleanup(self) -> None:
""" """
Stops all running telegram threads. Stops all running telegram threads.
@ -637,6 +643,25 @@ class Telegram(RPCHandler):
except RPCException as e: except RPCException as e:
self._send_msg(str(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:
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],
cols=3) -> List[List[InlineKeyboardButton]]:
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@authorized_only @authorized_only
def _forcebuy(self, update: Update, context: CallbackContext) -> None: def _forcebuy(self, update: Update, context: CallbackContext) -> None:
""" """
@ -649,10 +674,13 @@ class Telegram(RPCHandler):
if context.args: if context.args:
pair = context.args[0] pair = context.args[0]
price = float(context.args[1]) if len(context.args) > 1 else None price = float(context.args[1]) if len(context.args) > 1 else None
try: self._forcebuy_action(pair, price)
self._rpc._rpc_forcebuy(pair, price) else:
except RPCException as e: whitelist = self._rpc._rpc_whitelist()['whitelist']
self._send_msg(str(e)) 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 @authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None: def _trades(self, update: Update, context: CallbackContext) -> None:
@ -987,8 +1015,9 @@ class Telegram(RPCHandler):
f"*Current state:* `{val['state']}`" f"*Current state:* `{val['state']}`"
) )
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, def _send_inline_msg(self, msg: str, callback_query_handler,
disable_notification: bool = False) -> None: parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False,
keyboard: List[List[InlineKeyboardButton]] = None, ) -> None:
""" """
Send given markdown message Send given markdown message
:param msg: message :param msg: message
@ -996,7 +1025,29 @@ class Telegram(RPCHandler):
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) 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,
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, InlineKeyboardButton]]] = None,
reply_markup=ReplyKeyboardMarkup) -> None:
"""
Send given markdown message
:param msg: message
:param bot: alternative bot
:param parse_mode: telegram parse mode
:return: None
"""
if keyboard is None:
keyboard = self._keyboard
reply_markup = reply_markup(keyboard, resize_keyboard=True)
try: try:
try: try:
self._updater.bot.send_message( self._updater.bot.send_message(

View File

@ -4,6 +4,7 @@
import re import re
from datetime import datetime from datetime import datetime
from functools import reduce
from random import choice, randint from random import choice, randint
from string import ascii_uppercase from string import ascii_uppercase
from unittest.mock import ANY, MagicMock from unittest.mock import ANY, MagicMock
@ -54,6 +55,14 @@ class DummyCls(Telegram):
raise Exception('test') 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): def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
msg_mock = MagicMock() msg_mock = MagicMock()
if mock: if mock:
@ -902,6 +911,33 @@ def test_forcebuy_handle_exception(default_conf, update, mocker) -> None:
assert msg_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' 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
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, def test_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None: limit_buy_order, limit_sell_order, mocker) -> None: