Merge pull request #4746 from gmatheu/contribution/telegram_forcebuy_inline_keyboard
Telegram: forcebuy inline keyboard
This commit is contained in:
commit
ae037b0ec1
BIN
docs/assets/telegram_forcebuy.png
Normal file
BIN
docs/assets/telegram_forcebuy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -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)
|
||||||
@ -261,12 +265,12 @@ Note that for this to work, `forcebuy_enable` needs to be set to true.
|
|||||||
### /performance
|
### /performance
|
||||||
|
|
||||||
Return the performance of each crypto-currency the bot has sold.
|
Return the performance of each crypto-currency the bot has sold.
|
||||||
> Performance:
|
> Performance:
|
||||||
> 1. `RCN/BTC 0.003 BTC (57.77%) (1)`
|
> 1. `RCN/BTC 0.003 BTC (57.77%) (1)`
|
||||||
> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)`
|
> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)`
|
||||||
> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)`
|
> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)`
|
||||||
> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)`
|
> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)`
|
||||||
> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)`
|
> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)`
|
||||||
> ...
|
> ...
|
||||||
|
|
||||||
### /balance
|
### /balance
|
||||||
|
@ -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(
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user