diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7e276d538..6706e94ab 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -107,8 +107,7 @@ class Exchange: # Lock event loop. This is necessary to avoid race-conditions when using force* commands # Due to funding fee fetching. self._loop_lock = Lock() - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) + self.loop = self._init_async_loop() self._config: Config = {} self._config.update(config) @@ -212,6 +211,11 @@ class Exchange: if self.loop and not self.loop.is_closed(): self.loop.close() + def _init_async_loop(self) -> asyncio.AbstractEventLoop: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + def validate_config(self, config): # Check if timeframe is available self.validate_timeframes(config.get('timeframe')) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e99501cc0..6e509950c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ +import asyncio import json import logging import re @@ -13,15 +14,17 @@ from functools import partial from html import escape from itertools import chain from math import isnan -from typing import Any, Callable, Dict, List, Optional, Union +from threading import Thread +from typing import Any, Callable, Coroutine, Dict, List, Optional, Union import arrow from tabulate import tabulate -from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, - KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update) +from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, + ReplyKeyboardMarkup, Update) +from telegram.constants import MessageLimit, ParseMode from telegram.error import BadRequest, NetworkError, TelegramError -from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater -from telegram.utils.helpers import escape_markdown +from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler +from telegram.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN, Config @@ -33,6 +36,9 @@ from freqtrade.rpc import RPC, RPCException, RPCHandler from freqtrade.rpc.rpc_types import RPCSendMsg +MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH + + logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') @@ -47,14 +53,14 @@ class TimeunitMappings: default: int -def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: +def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): """ Decorator to check if the message comes from the correct chat_id :param command_handler: Telegram CommandHandler :return: decorated function """ - def wrapper(self, *args, **kwargs): + async def wrapper(self, *args, **kwargs): """ Decorator logic """ update = kwargs.get('update') or args[0] @@ -76,9 +82,9 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: chat_id ) try: - return command_handler(self, *args, **kwargs) + return await command_handler(self, *args, **kwargs) except RPCException as e: - self._send_msg(str(e)) + await self._send_msg(str(e)) except BaseException: logger.exception('Exception occurred within Telegram module') finally: @@ -99,9 +105,17 @@ class Telegram(RPCHandler): """ super().__init__(rpc, config) - self._updater: Updater + self._app: Application + self._loop: asyncio.AbstractEventLoop self._init_keyboard() - self._init() + self._start_thread() + + def _start_thread(self): + """ + Creates and starts the polling thread + """ + self._thread = Thread(target=self._init, name='FTTelegram') + self._thread.start() def _init_keyboard(self) -> None: """ @@ -152,14 +166,23 @@ class Telegram(RPCHandler): logger.info('using custom keyboard from ' f'config.json: {self._keyboard}') + def _init_telegram_app(self): + return Application.builder().token(self._config['telegram']['token']).build() + def _init(self) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates + Runs in a separate thread. """ - self._updater = Updater(token=self._config['telegram']['token'], workers=0, - use_context=True) + try: + self._loop = asyncio.get_running_loop() + except RuntimeError: + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + + self._app = self._init_telegram_app() # Register command handler and start telegram message polling handles = [ @@ -218,21 +241,38 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"), ] for handle in handles: - self._updater.dispatcher.add_handler(handle) + self._app.add_handler(handle) for callback in callbacks: - self._updater.dispatcher.add_handler(callback) + self._app.add_handler(callback) - self._updater.start_polling( - bootstrap_retries=-1, - timeout=20, - read_latency=60, # Assumed transmission latency - drop_pending_updates=True, - ) logger.info( 'rpc.telegram is listening for following commands: %s', - [h.command for h in handles] + [[x for x in sorted(h.commands)] for h in handles] ) + self._loop.run_until_complete(self._startup_telegram()) + + async def _startup_telegram(self) -> None: + await self._app.initialize() + await self._app.start() + if self._app.updater: + await self._app.updater.start_polling( + bootstrap_retries=-1, + timeout=20, + # read_latency=60, # Assumed transmission latency + drop_pending_updates=True, + # stop_signals=[], # Necessary as we don't run on the main thread + ) + while True: + await asyncio.sleep(10) + if not self._app.updater.running: + break + + async def _cleanup_telegram(self) -> None: + if self._app.updater: + await self._app.updater.stop() + await self._app.stop() + await self._app.shutdown() def cleanup(self) -> None: """ @@ -240,7 +280,8 @@ class Telegram(RPCHandler): :return: None """ # This can take up to `timeout` from the call to `start_polling`. - self._updater.stop() + asyncio.run_coroutine_threadsafe(self._cleanup_telegram(), self._loop) + self._thread.join() def _exchange_from_msg(self, msg: Dict[str, Any]) -> str: """ @@ -453,7 +494,9 @@ class Telegram(RPCHandler): message = self.compose_message(deepcopy(msg), msg_type) # type: ignore if message: - self._send_msg(message, disable_notification=(noti == 'silent')) + asyncio.run_coroutine_threadsafe( + self._send_msg(message, disable_notification=(noti == 'silent')), + self._loop) def _get_sell_emoji(self, msg): """ @@ -536,7 +579,7 @@ class Telegram(RPCHandler): return lines_detail @authorized_only - def _status(self, update: Update, context: CallbackContext) -> None: + async def _status(self, update: Update, context: CallbackContext) -> None: """ Handler for /status. Returns the current TradeThread status @@ -546,12 +589,12 @@ class Telegram(RPCHandler): """ if context.args and 'table' in context.args: - self._status_table(update, context) + await self._status_table(update, context) return else: - self._status_msg(update, context) + await self._status_msg(update, context) - def _status_msg(self, update: Update, context: CallbackContext) -> None: + async def _status_msg(self, update: Update, context: CallbackContext) -> None: """ handler for `/status` and `/status `. @@ -635,9 +678,9 @@ class Telegram(RPCHandler): lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) lines.extend(lines_detail if lines_detail else "") - self.__send_status_msg(lines, r) + await self.__send_status_msg(lines, r) - def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: + async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: """ Send status message. """ @@ -648,13 +691,13 @@ class Telegram(RPCHandler): if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: msg += line + '\n' else: - self._send_msg(msg.format(**r)) + await self._send_msg(msg.format(**r)) msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n' - self._send_msg(msg.format(**r)) + await self._send_msg(msg.format(**r)) @authorized_only - def _status_table(self, update: Update, context: CallbackContext) -> None: + async def _status_table(self, update: Update, context: CallbackContext) -> None: """ Handler for /status table. Returns the current TradeThread status in table format @@ -687,12 +730,11 @@ class Telegram(RPCHandler): # insert separators line between Total lines = message.split("\n") message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) - self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_status_table", - query=update.callback_query) + await self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_status_table", + query=update.callback_query) - @authorized_only - def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: + async def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. @@ -739,11 +781,11 @@ class Telegram(RPCHandler): f'{val.message} Profit over the last {timescale} {val.message2}:\n' f'
{stats_tab}
' ) - self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, - callback_path=val.callback, query=update.callback_query) + await self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path=val.callback, query=update.callback_query) @authorized_only - def _daily(self, update: Update, context: CallbackContext) -> None: + async def _daily(self, update: Update, context: CallbackContext) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. @@ -751,10 +793,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'days') + await self._timeunit_stats(update, context, 'days') @authorized_only - def _weekly(self, update: Update, context: CallbackContext) -> None: + async def _weekly(self, update: Update, context: CallbackContext) -> None: """ Handler for /weekly Returns a weekly profit (in BTC) over the last n weeks. @@ -762,10 +804,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'weeks') + await self._timeunit_stats(update, context, 'weeks') @authorized_only - def _monthly(self, update: Update, context: CallbackContext) -> None: + async def _monthly(self, update: Update, context: CallbackContext) -> None: """ Handler for /monthly Returns a monthly profit (in BTC) over the last n months. @@ -773,10 +815,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'months') + await self._timeunit_stats(update, context, 'months') @authorized_only - def _profit(self, update: Update, context: CallbackContext) -> None: + async def _profit(self, update: Update, context: CallbackContext) -> None: """ Handler for /profit. Returns a cumulative profit statistics. @@ -850,11 +892,11 @@ class Telegram(RPCHandler): f"*Max Drawdown:* `{stats['max_drawdown']:.2%} " f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`" ) - self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", - query=update.callback_query) + await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", + query=update.callback_query) @authorized_only - def _stats(self, update: Update, context: CallbackContext) -> None: + async def _stats(self, update: Update, context: CallbackContext) -> None: """ Handler for /stats Show stats of recent trades @@ -885,7 +927,7 @@ class Telegram(RPCHandler): headers=['Exit Reason', 'Exits', 'Wins', 'Losses'] ) if len(exit_reasons_tabulate) > 25: - self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) + await self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) exit_reasons_msg = '' durations = stats['durations'] @@ -900,10 +942,10 @@ class Telegram(RPCHandler): ) msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""") - self._send_msg(msg, ParseMode.MARKDOWN) + await self._send_msg(msg, ParseMode.MARKDOWN) @authorized_only - def _balance(self, update: Update, context: CallbackContext) -> None: + async def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ full_result = context.args and 'full' in context.args result = self._rpc._rpc_balance(self._config['stake_currency'], @@ -957,7 +999,7 @@ class Telegram(RPCHandler): # Handle overflowing message length if len(output + curr_output) >= MAX_MESSAGE_LENGTH: - self._send_msg(output) + await self._send_msg(output) output = curr_output else: output += curr_output @@ -981,11 +1023,11 @@ class Telegram(RPCHandler): f"\t`{result['stake']}: {total_stake}`{stake_improve}\n" f"\t`{result['symbol']}: {value}`{fiat_val}\n" ) - self._send_msg(output, reload_able=True, callback_path="update_balance", - query=update.callback_query) + await self._send_msg(output, reload_able=True, callback_path="update_balance", + query=update.callback_query) @authorized_only - def _start(self, update: Update, context: CallbackContext) -> None: + async def _start(self, update: Update, context: CallbackContext) -> None: """ Handler for /start. Starts TradeThread @@ -994,10 +1036,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_start() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _stop(self, update: Update, context: CallbackContext) -> None: + async def _stop(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop. Stops TradeThread @@ -1006,10 +1048,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_stop() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _reload_config(self, update: Update, context: CallbackContext) -> None: + async def _reload_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /reload_config. Triggers a config file reload @@ -1018,10 +1060,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_reload_config() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _stopentry(self, update: Update, context: CallbackContext) -> None: + async def _stopentry(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop_buy. Sets max_open_trades to 0 and gracefully sells all open trades @@ -1030,10 +1072,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_stopentry() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _force_exit(self, update: Update, context: CallbackContext) -> None: + async def _force_exit(self, update: Update, context: CallbackContext) -> None: """ Handler for /forceexit . Sells the given trade at current price @@ -1044,14 +1086,14 @@ class Telegram(RPCHandler): if context.args: trade_id = context.args[0] - self._force_exit_action(trade_id) + await self._force_exit_action(trade_id) else: fiat_currency = self._config.get('fiat_display_currency', '') try: statlist, _, _ = self._rpc._rpc_status_table( self._config['stake_currency'], fiat_currency) except RPCException: - self._send_msg(msg='No open trade found.') + await self._send_msg(msg='No open trade found.') return trades = [] for trade in statlist: @@ -1064,51 +1106,51 @@ class Telegram(RPCHandler): buttons_aligned.append([InlineKeyboardButton( text='Cancel', callback_data='force_exit__cancel')]) - self._send_msg(msg="Which trade?", keyboard=buttons_aligned) + await self._send_msg(msg="Which trade?", keyboard=buttons_aligned) - def _force_exit_action(self, trade_id): + async 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)) + await self._send_msg(str(e)) - def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: + async 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__" trade_id = query.data.split("__")[1].split(' ')[0] if trade_id == 'cancel': - query.answer() - query.edit_message_text(text="Force exit canceled.") + await query.answer() + await query.edit_message_text(text="Force exit canceled.") return trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first() - query.answer() + await query.answer() if trade: - query.edit_message_text( + await query.edit_message_text( text=f"Manually exiting Trade #{trade_id}, {trade.pair}") - self._force_exit_action(trade_id) + await self._force_exit_action(trade_id) else: - query.edit_message_text(text=f"Trade {trade_id} not found.") + await query.edit_message_text(text=f"Trade {trade_id} not found.") - def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): + async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': try: self._rpc._rpc_force_entry(pair, price, order_side=order_side) except RPCException as e: logger.exception("Forcebuy error!") - self._send_msg(str(e), ParseMode.HTML) + await self._send_msg(str(e), ParseMode.HTML) - def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: + async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query if query.data and '_||_' in query.data: pair, side = query.data.split('_||_') order_side = SignalDirection(side) - query.answer() - query.edit_message_text(text=f"Manually entering {order_side} for {pair}") - self._force_enter_action(pair, None, order_side) + await query.answer() + await query.edit_message_text(text=f"Manually entering {order_side} for {pair}") + await self._force_enter_action(pair, None, order_side) @staticmethod def _layout_inline_keyboard( @@ -1121,7 +1163,7 @@ class Telegram(RPCHandler): return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @authorized_only - def _force_enter( + async def _force_enter( self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None: """ Handler for /forcelong and `/forceshort @@ -1133,7 +1175,7 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None - self._force_enter_action(pair, price, order_side) + await self._force_enter_action(pair, price, order_side) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] pair_buttons = [ @@ -1143,12 +1185,12 @@ 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, - query=update.callback_query) + await self._send_msg(msg="Which pair?", + keyboard=buttons_aligned, + query=update.callback_query) @authorized_only - def _trades(self, update: Update, context: CallbackContext) -> None: + async def _trades(self, update: Update, context: CallbackContext) -> None: """ Handler for /trades Returns last n recent trades. @@ -1177,10 +1219,10 @@ class Telegram(RPCHandler): tablefmt='simple') message = (f"{min(trades['trades_count'], nrecent)} recent trades:\n" + (f"
{trades_tab}
" if trades['trades_count'] > 0 else '')) - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _delete_trade(self, update: Update, context: CallbackContext) -> None: + async def _delete_trade(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete . Delete the given trade @@ -1192,13 +1234,13 @@ class Telegram(RPCHandler): raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) msg = self._rpc._rpc_delete(trade_id) - self._send_msg( + await self._send_msg( f"`{msg['result_msg']}`\n" 'Please make sure to take care of this asset on the exchange manually.' ) @authorized_only - def _cancel_open_order(self, update: Update, context: CallbackContext) -> None: + async def _cancel_open_order(self, update: Update, context: CallbackContext) -> None: """ Handler for /cancel_open_order . Cancel open order for tradeid @@ -1210,10 +1252,10 @@ class Telegram(RPCHandler): raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) self._rpc._rpc_cancel_open_order(trade_id) - self._send_msg('Open order canceled.') + await self._send_msg('Open order canceled.') @authorized_only - def _performance(self, update: Update, context: CallbackContext) -> None: + async def _performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /performance. Shows a performance statistic from finished trades @@ -1231,17 +1273,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_performance", + query=update.callback_query) @authorized_only - def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: + async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /buys PAIR . Shows a performance statistic from finished trades @@ -1263,17 +1305,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_enter_tag_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_enter_tag_performance", + query=update.callback_query) @authorized_only - def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: + async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /sells. Shows a performance statistic from finished trades @@ -1295,17 +1337,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_exit_reason_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_exit_reason_performance", + query=update.callback_query) @authorized_only - def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: + async def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /mix_tags. Shows a performance statistic from finished trades @@ -1327,17 +1369,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_mix_tag_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_mix_tag_performance", + query=update.callback_query) @authorized_only - def _count(self, update: Update, context: CallbackContext) -> None: + async def _count(self, update: Update, context: CallbackContext) -> None: """ Handler for /count. Returns the number of trades running @@ -1351,19 +1393,19 @@ class Telegram(RPCHandler): tablefmt='simple') message = f"
{message}
" logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_count", - query=update.callback_query) + await self._send_msg(message, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_count", + query=update.callback_query) @authorized_only - def _locks(self, update: Update, context: CallbackContext) -> None: + async def _locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /locks. Returns the currently active locks """ rpc_locks = self._rpc._rpc_locks() if not rpc_locks['locks']: - self._send_msg('No active locks.', parse_mode=ParseMode.HTML) + await self._send_msg('No active locks.', parse_mode=ParseMode.HTML) for locks in chunks(rpc_locks['locks'], 25): message = tabulate([[ @@ -1375,10 +1417,10 @@ class Telegram(RPCHandler): tablefmt='simple') message = f"
{escape(message)}
" logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _delete_locks(self, update: Update, context: CallbackContext) -> None: + async def _delete_locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete_locks. Returns the currently active locks @@ -1393,10 +1435,10 @@ class Telegram(RPCHandler): pair = arg self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) - self._locks(update, context) + await self._locks(update, context) @authorized_only - def _whitelist(self, update: Update, context: CallbackContext) -> None: + async def _whitelist(self, update: Update, context: CallbackContext) -> None: """ Handler for /whitelist Shows the currently active whitelist @@ -1413,39 +1455,39 @@ class Telegram(RPCHandler): message += f"`{', '.join(whitelist['whitelist'])}`" logger.debug(message) - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _blacklist(self, update: Update, context: CallbackContext) -> None: + async def _blacklist(self, update: Update, context: CallbackContext) -> None: """ Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) + await self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) - def send_blacklist_msg(self, blacklist: Dict): + async def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] for pair, error in blacklist['errors'].items(): errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: - self._send_msg('\n'.join(errmsgs)) + await self._send_msg('\n'.join(errmsgs)) message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" logger.debug(message) - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: + async def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: """ Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) + await self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) @authorized_only - def _logs(self, update: Update, context: CallbackContext) -> None: + async def _logs(self, update: Update, context: CallbackContext) -> None: """ Handler for /logs Shows the latest logs @@ -1464,17 +1506,17 @@ class Telegram(RPCHandler): escape_markdown(logrec[4], version=2)) if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH: # Send message immediately if it would become too long - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) msgs = msg + '\n' else: # Append message to messages to send msgs += msg + '\n' if msgs: - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) @authorized_only - def _edge(self, update: Update, context: CallbackContext) -> None: + async def _edge(self, update: Update, context: CallbackContext) -> None: """ Handler for /edge Shows information related to Edge @@ -1482,17 +1524,17 @@ class Telegram(RPCHandler): edge_pairs = self._rpc._rpc_edge() if not edge_pairs: message = 'Edge only validated following pairs:' - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) for chunk in chunks(edge_pairs, 25): edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple') message = (f'Edge only validated following pairs:\n' f'
{edge_pairs_tab}
') - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _help(self, update: Update, context: CallbackContext) -> None: + async def _help(self, update: Update, context: CallbackContext) -> None: """ Handler for /help. Show commands of the bot @@ -1570,20 +1612,20 @@ class Telegram(RPCHandler): "*/version:* `Show version`" ) - self._send_msg(message, parse_mode=ParseMode.MARKDOWN) + await self._send_msg(message, parse_mode=ParseMode.MARKDOWN) @authorized_only - def _health(self, update: Update, context: CallbackContext) -> None: + async def _health(self, update: Update, context: CallbackContext) -> None: """ Handler for /health Shows the last process timestamp """ health = self._rpc.health() message = f"Last process: `{health['last_process_loc']}`" - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _version(self, update: Update, context: CallbackContext) -> None: + async def _version(self, update: Update, context: CallbackContext) -> None: """ Handler for /version. Show version information @@ -1596,10 +1638,10 @@ class Telegram(RPCHandler): if strategy_version is not None: version_string += f', *Strategy version: * `{strategy_version}`' - self._send_msg(version_string) + await self._send_msg(version_string) @authorized_only - def _show_config(self, update: Update, context: CallbackContext) -> None: + async def _show_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /show_config. Show config information information @@ -1628,7 +1670,7 @@ class Telegram(RPCHandler): else: pa_info = "*Position adjustment:* Off\n" - self._send_msg( + await self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Market: * `{val['trading_mode']}`\n" @@ -1644,8 +1686,8 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) - def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", - reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: + async def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", + reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: if reload_able: reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("Refresh", callback_data=callback_path)], @@ -1659,7 +1701,7 @@ class Telegram(RPCHandler): message_id = query.message.message_id try: - self._updater.bot.edit_message_text( + await self._app.bot.edit_message_text( chat_id=chat_id, message_id=message_id, text=msg, @@ -1674,12 +1716,12 @@ class Telegram(RPCHandler): except TelegramError as telegram_err: logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message) - def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False, - keyboard: Optional[List[List[InlineKeyboardButton]]] = None, - callback_path: str = "", - reload_able: bool = False, - query: Optional[CallbackQuery] = None) -> None: + async def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, + disable_notification: bool = False, + keyboard: Optional[List[List[InlineKeyboardButton]]] = None, + callback_path: str = "", + reload_able: bool = False, + query: Optional[CallbackQuery] = None) -> None: """ Send given markdown message :param msg: message @@ -1689,20 +1731,20 @@ class Telegram(RPCHandler): """ reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] if query: - self._update_msg(query=query, msg=msg, parse_mode=parse_mode, - callback_path=callback_path, reload_able=reload_able) + await self._update_msg(query=query, msg=msg, parse_mode=parse_mode, + callback_path=callback_path, reload_able=reload_able) return if reload_able and self._config['telegram'].get('reload', True): reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("Refresh", callback_data=callback_path)]]) else: if keyboard is not None: - reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True) + reply_markup = InlineKeyboardMarkup(keyboard) else: reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) try: try: - self._updater.bot.send_message( + await self._app.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, @@ -1716,7 +1758,7 @@ class Telegram(RPCHandler): 'Telegram NetworkError: %s! Trying one more time.', network_err.message ) - self._updater.bot.send_message( + await self._app.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, @@ -1730,7 +1772,7 @@ class Telegram(RPCHandler): ) @authorized_only - def _changemarketdir(self, update: Update, context: CallbackContext) -> None: + async def _changemarketdir(self, update: Update, context: CallbackContext) -> None: """ Handler for /marketdir. Updates the bot's market_direction @@ -1753,14 +1795,14 @@ class Telegram(RPCHandler): if new_market_dir is not None: self._rpc._update_market_direction(new_market_dir) - self._send_msg("Successfully updated market direction" - f" from *{old_market_dir}* to *{new_market_dir}*.") + await self._send_msg("Successfully updated market direction" + f" from *{old_market_dir}* to *{new_market_dir}*.") else: raise RPCException("Invalid market direction provided. \n" "Valid market directions: *long, short, even, none*") elif context.args is not None and len(context.args) == 0: old_market_dir = self._rpc._get_market_direction() - self._send_msg(f"Currently set market direction: *{old_market_dir}*") + await self._send_msg(f"Currently set market direction: *{old_market_dir}*") else: raise RPCException("Invalid usage of command /marketdir. \n" "Usage: */marketdir [short | long | even | none]*") diff --git a/requirements-dev.txt b/requirements-dev.txt index cd4c96eea..ea75bb8f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,8 +18,6 @@ pytest-random-order==1.1.0 isort==5.12.0 # For datetime mocking time-machine==2.9.0 -# fastapi testing -httpx==0.24.0 # Convert jupyter notebooks to markdown documents nbconvert==7.3.1 diff --git a/requirements.txt b/requirements.txt index 9d51852fc..6eecfb630 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,9 @@ ccxt==3.0.75 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.10 -python-telegram-bot==13.15 +python-telegram-bot==20.2 +# can't be hard-pinned due to telegram-bot pinning httpx with ~ +httpx>=0.23.3 arrow==1.2.3 cachetools==4.2.2 requests==2.28.2 diff --git a/tests/conftest.py b/tests/conftest.py index 2f2345d54..1c737b3aa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import json import logging import re from copy import deepcopy -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path from typing import Optional from unittest.mock import MagicMock, Mock, PropertyMock @@ -12,7 +12,6 @@ import arrow import numpy as np import pandas as pd import pytest -from telegram import Chat, Message, Update from freqtrade import constants from freqtrade.commands import Arguments @@ -550,13 +549,6 @@ def get_default_conf_usdt(testdatadir): return configuration -@pytest.fixture -def update(): - _update = Update(0) - _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) - return _update - - @pytest.fixture def fee(): return MagicMock(return_value=0.0025) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e045bf487..8123e4689 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -283,7 +283,7 @@ def test_api__init__(default_conf, mocker): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock()) apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) @@ -341,7 +341,7 @@ def test_api_run(default_conf, mocker, caplog): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') server_inst_mock = MagicMock() server_inst_mock.run_in_thread = MagicMock() @@ -419,7 +419,7 @@ def test_api_cleanup(default_conf, mocker, caplog): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') server_mock = MagicMock() server_mock.cleanup = MagicMock() @@ -1877,7 +1877,7 @@ def test_api_ws_send_msg(default_conf, mocker, caplog): "password": _TEST_PASS, "ws_token": _TEST_WS_TOKEN }}) - mocker.patch('freqtrade.rpc.telegram.Updater') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api') apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 9b22b73c0..4b4c2b028 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2,13 +2,15 @@ # pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=too-many-lines, too-many-arguments +import asyncio import logging import re +import threading from datetime import datetime, timedelta, timezone from functools import reduce from random import choice, randint from string import ascii_uppercase -from unittest.mock import ANY, MagicMock +from unittest.mock import ANY, AsyncMock, MagicMock import arrow import pytest @@ -36,6 +38,11 @@ from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, patch_exchange, patch_get_signal, patch_whitelist) +@pytest.fixture(autouse=True) +def mock_exchange_loop(mocker): + mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop') + + @pytest.fixture def default_conf(default_conf) -> dict: # Telegram is enabled by default @@ -43,6 +50,28 @@ def default_conf(default_conf) -> dict: return default_conf +@pytest.fixture +def update(): + message = Message(0, datetime.utcnow(), Chat(0, 0)) + _update = Update(0, message=message) + + return _update + + +def patch_eventloop_threading(telegrambot): + is_init = False + + def thread_fuck(): + nonlocal is_init + telegrambot._loop = asyncio.new_event_loop() + is_init = True + telegrambot._loop.run_forever() + x = threading.Thread(target=thread_fuck, daemon=True) + x.start() + while not is_init: + pass + + class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator @@ -56,14 +85,14 @@ class DummyCls(Telegram): pass @authorized_only - def dummy_handler(self, *args, **kwargs) -> None: + async def dummy_handler(self, *args, **kwargs) -> None: """ Fake method that only change the state of the object """ self.state['called'] = True @authorized_only - def dummy_exception(self, *args, **kwargs) -> None: + async def dummy_exception(self, *args, **kwargs) -> None: """ Fake method that throw an exception """ @@ -71,23 +100,26 @@ class DummyCls(Telegram): def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): - msg_mock = MagicMock() + msg_mock = AsyncMock() if mock: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _send_msg=msg_mock + _send_msg=msg_mock, + _start_thread=MagicMock(), ) if not ftbot: + mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop') ftbot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(ftbot) telegram = Telegram(rpc, default_conf) + telegram._loop = MagicMock() + patch_eventloop_threading(telegram) return telegram, ftbot, msg_mock def test_telegram__init__(default_conf, mocker) -> None: - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram, _, _ = get_telegram_testobject(mocker, default_conf) @@ -95,43 +127,73 @@ def test_telegram__init__(default_conf, mocker) -> None: def test_telegram_init(default_conf, mocker, caplog) -> None: - start_polling = MagicMock() - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) + app_mock = MagicMock() + mocker.patch('freqtrade.rpc.telegram.Telegram._start_thread', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init_telegram_app', return_value=app_mock) + mocker.patch('freqtrade.rpc.telegram.Telegram._startup_telegram', AsyncMock()) - get_telegram_testobject(mocker, default_conf, mock=False) - assert start_polling.call_count == 0 + telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) + telegram._init() + assert app_mock.call_count == 0 # number of handles registered - assert start_polling.dispatcher.add_handler.call_count > 0 - assert start_polling.start_polling.call_count == 1 + assert app_mock.add_handler.call_count > 0 + # assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], " - "['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " - "['trades'], ['delete'], ['coo', 'cancel_open_order'], ['performance'], " - "['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], " + "['forceexit', 'forcesell', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " + "['trades'], ['delete'], ['cancel_open_order', 'coo'], ['performance'], " + "['buys', 'entries'], ['exits', 'sells'], ['mix_tags'], " "['stats'], ['daily'], ['weekly'], ['monthly'], " - "['count'], ['locks'], ['unlock', 'delete_locks'], " - "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " + "['count'], ['locks'], ['delete_locks', 'unlock'], " + "['reload_conf', 'reload_config'], ['show_conf', 'show_config'], " "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " - "['blacklist_delete', 'bl_delete'], " + "['bl_delete', 'blacklist_delete'], " "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir']" "]") assert log_has(message_str, caplog) -def test_cleanup(default_conf, mocker, ) -> None: +async def test_telegram_startup(default_conf, mocker) -> None: + app_mock = MagicMock() + app_mock.initialize = AsyncMock() + app_mock.start = AsyncMock() + app_mock.updater.start_polling = AsyncMock() + app_mock.updater.running = False + sleep_mock = mocker.patch('freqtrade.rpc.telegram.asyncio.sleep', AsyncMock()) + + telegram, _, _ = get_telegram_testobject(mocker, default_conf) + telegram._app = app_mock + await telegram._startup_telegram() + assert app_mock.initialize.call_count == 1 + assert app_mock.start.call_count == 1 + assert app_mock.updater.start_polling.call_count == 1 + assert sleep_mock.call_count == 1 + + +async def test_telegram_cleanup(default_conf, mocker, ) -> None: + app_mock = MagicMock() + app_mock.stop = AsyncMock() + app_mock.initialize = AsyncMock() + updater_mock = MagicMock() - updater_mock.stop = MagicMock() - mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) + updater_mock.stop = AsyncMock() + app_mock.updater = updater_mock + # mocker.patch('freqtrade.rpc.telegram.Application', app_mock) - telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) + telegram, _, _ = get_telegram_testobject(mocker, default_conf) + telegram._app = app_mock + telegram._loop = asyncio.get_running_loop() + telegram._thread = MagicMock() telegram.cleanup() - assert telegram._updater.stop.call_count == 1 + await asyncio.sleep(0.1) + assert app_mock.stop.call_count == 1 + assert telegram._thread.join.call_count == 1 -def test_authorized_only(default_conf, mocker, caplog, update) -> None: +async def test_authorized_only(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) caplog.set_level(logging.DEBUG) default_conf['telegram']['enabled'] = False @@ -140,19 +202,19 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None: dummy = DummyCls(rpc, default_conf) patch_get_signal(bot) - dummy.dummy_handler(update=update, context=MagicMock()) + await dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is True assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog) assert not log_has('Rejected unauthorized message from: 0', caplog) assert not log_has('Exception occurred within Telegram module', caplog) -def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: +async def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: patch_exchange(mocker) caplog.set_level(logging.DEBUG) chat = Chat(0xdeadbeef, 0) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), datetime.utcnow(), chat) + message = Message(randint(1, 100), datetime.utcnow(), chat) + update = Update(randint(1, 100), message=message) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -160,14 +222,14 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: dummy = DummyCls(rpc, default_conf) patch_get_signal(bot) - dummy.dummy_handler(update=update, context=MagicMock()) + await dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is False assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog) assert log_has('Rejected unauthorized message from: 3735928559', caplog) assert not log_has('Exception occurred within Telegram module', caplog) -def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None: +async def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) default_conf['telegram']['enabled'] = False @@ -177,17 +239,15 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None dummy = DummyCls(rpc, default_conf) patch_get_signal(bot) - dummy.dummy_exception(update=update, context=MagicMock()) + await dummy.dummy_exception(update=update, context=MagicMock()) assert dummy.state['called'] is False assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog) assert not log_has('Rejected unauthorized message from: 0', caplog) assert log_has('Exception occurred within Telegram module', caplog) -def test_telegram_status(default_conf, update, mocker) -> None: - update.message.chat.id = "123" +async def test_telegram_status(default_conf, update, mocker) -> None: default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" status_table = MagicMock() mocker.patch('freqtrade.rpc.telegram.Telegram._status_table', status_table) @@ -232,21 +292,19 @@ def test_telegram_status(default_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 context = MagicMock() # /status table context.args = ["table"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert status_table.call_count == 1 @pytest.mark.usefixtures("init_persistence") -def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: - update.message.chat.id = "123" +async def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" default_conf['position_adjustment_enable'] = True mocker.patch.multiple( EXMS, @@ -284,7 +342,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: trade.recalc_trade_from_orders() Trade.commit() - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 4 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Number of Entries.*2', msg) @@ -296,10 +354,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: @pytest.mark.usefixtures("init_persistence") -def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: - update.message.chat.id = "123" - default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" +async def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: default_conf['position_adjustment_enable'] = True mocker.patch.multiple( EXMS, @@ -313,14 +368,14 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None trade = Trade.get_trades([Trade.is_open.is_(False)]).first() context = MagicMock() context.args = [str(trade.id)] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert msg_mock.call_count == 1 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Close Date:', msg) assert re.search(r'Close Profit:', msg) -def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 3 mocker.patch.multiple( EXMS, @@ -340,13 +395,13 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED # Status is also enabled when stopped - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -354,7 +409,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() # Trigger status while we have a fulfilled order for the open trade - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) # close_rate should not be included in the message as the trade is not closed # and no line should be empty @@ -371,7 +426,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: context = MagicMock() context.args = ["2", "3"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) lines = msg_mock.call_args_list[0][0][0].split('\n') assert '' not in lines[:-1] @@ -386,7 +441,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() context = MagicMock() context.args = ["2"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert msg_mock.call_count == 2 @@ -398,7 +453,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: assert 'Trade ID:* `2` - continued' in msg2 -def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -413,13 +468,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED # Status table is also enabled when stopped - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -427,7 +482,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) text = re.sub('', '', msg_mock.call_args_list[-1][0][0]) line = text.split("\n") @@ -439,7 +494,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg_mock.call_count == 1 -def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1 @@ -461,7 +516,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # /daily 2 context = MagicMock() context.args = ["2"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] assert 'Day ' in msg_mock.call_args_list[0][0][0] @@ -475,7 +530,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] @@ -492,13 +547,13 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # /daily 1 context = MagicMock() context.args = ["1"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] -def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: +async def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker @@ -513,7 +568,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # /daily -2 context = MagicMock() context.args = ["-2"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -523,11 +578,11 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # /daily today context = MagicMock() context.args = ["today"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert 'Daily Profit over the last 7 days:' in msg_mock.call_args_list[0][0][0] -def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -548,7 +603,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly 2 context = MagicMock() context.args = ["2"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert "Weekly Profit over the last 2 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] @@ -564,7 +619,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert "Weekly Profit over the last 8 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] @@ -580,7 +635,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly -3 context = MagicMock() context.args = ["-3"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -590,14 +645,14 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly this week context = MagicMock() context.args = ["this week"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert ( 'Weekly Profit over the last 8 weeks (starting from Monday):' in msg_mock.call_args_list[0][0][0] ) -def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -618,7 +673,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly 2 context = MagicMock() context.args = ["2"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 2 months:' in msg_mock.call_args_list[0][0][0] assert 'Month ' in msg_mock.call_args_list[0][0][0] @@ -633,7 +688,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 # Default to 6 months assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] @@ -650,7 +705,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly 12 context = MagicMock() context.args = ["12"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] @@ -667,7 +722,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly -3 context = MagicMock() context.args = ["-3"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -677,11 +732,11 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly february context = MagicMock() context.args = ["february"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] -def test_telegram_profit_handle( +async def test_telegram_profit_handle( default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, limit_sell_order_usdt, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) @@ -694,7 +749,7 @@ def test_telegram_profit_handle( telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) patch_get_signal(freqtradebot) - telegram._profit(update=update, context=MagicMock()) + await telegram._profit(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -706,7 +761,7 @@ def test_telegram_profit_handle( context = MagicMock() # Test with invalid 2nd argument (should silently pass) context.args = ["aaa"] - telegram._profit(update=update, context=context) + await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] @@ -729,7 +784,7 @@ def test_telegram_profit_handle( Trade.commit() context.args = [3] - telegram._profit(update=update, context=context) + await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0] assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`' @@ -747,7 +802,7 @@ def test_telegram_profit_handle( @pytest.mark.parametrize('is_short', [True, False]) -def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: +async def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( EXMS, @@ -757,7 +812,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._stats(update=update, context=MagicMock()) + await telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -765,7 +820,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> # Create some test data create_mock_trades(fee, is_short=is_short) - telegram._stats(update=update, context=MagicMock()) + await telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Exit Reason' in msg_mock.call_args_list[-1][0][0] assert 'ROI' in msg_mock.call_args_list[-1][0][0] @@ -773,7 +828,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> msg_mock.reset_mock() -def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: +async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: default_conf['dry_run'] = False mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance) mocker.patch(f'{EXMS}.get_tickers', tickers) @@ -782,10 +837,10 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) context = MagicMock() context.args = ["full"] - telegram._balance(update=update, context=context) + await telegram._balance(update=update, context=context) result = msg_mock.call_args_list[0][0][0] result_full = msg_mock.call_args_list[1][0][0] assert msg_mock.call_count == 2 @@ -807,7 +862,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*Estimated Value (Bot managed assets only)*:' in result -def test_balance_handle_empty_response(default_conf, update, mocker) -> None: +async def test_balance_handle_empty_response(default_conf, update, mocker) -> None: default_conf['dry_run'] = False mocker.patch(f'{EXMS}.get_balances', return_value={}) @@ -815,26 +870,26 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: patch_get_signal(freqtradebot) freqtradebot.config['dry_run'] = False - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert 'Starting capital: `0 BTC' in result -def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: +async def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: mocker.patch(f'{EXMS}.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert "*Warning:* Simulated balances in Dry Mode." in result assert "Starting capital: `1000 BTC`" in result -def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: +async def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: balances = [] for i in range(100): curr = choice(ascii_uppercase) + choice(ascii_uppercase) + choice(ascii_uppercase) @@ -867,7 +922,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) assert msg_mock.call_count > 1 # Test if wrap happens around 4000 - # and each single currency-output is around 120 characters long so we need @@ -876,79 +931,79 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None assert len(msg_mock.call_args_list[0][0][0]) > (4096 - 120) -def test_start_handle(default_conf, update, mocker) -> None: +async def test_start_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED - telegram._start(update=update, context=MagicMock()) + await telegram._start(update=update, context=MagicMock()) assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 -def test_start_handle_already_running(default_conf, update, mocker) -> None: +async def test_start_handle_already_running(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._start(update=update, context=MagicMock()) + await telegram._start(update=update, context=MagicMock()) assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 assert 'already running' in msg_mock.call_args_list[0][0][0] -def test_stop_handle(default_conf, update, mocker) -> None: +async def test_stop_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._stop(update=update, context=MagicMock()) + await telegram._stop(update=update, context=MagicMock()) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'stopping trader' in msg_mock.call_args_list[0][0][0] -def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: +async def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED - telegram._stop(update=update, context=MagicMock()) + await telegram._stop(update=update, context=MagicMock()) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'already stopped' in msg_mock.call_args_list[0][0][0] -def test_stopbuy_handle(default_conf, update, mocker) -> None: +async def test_stopbuy_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) assert freqtradebot.config['max_open_trades'] != 0 - telegram._stopentry(update=update, context=MagicMock()) + await telegram._stopentry(update=update, context=MagicMock()) assert freqtradebot.config['max_open_trades'] == 0 assert msg_mock.call_count == 1 assert 'No more entries will occur from now. Run /reload_config to reset.' \ in msg_mock.call_args_list[0][0][0] -def test_reload_config_handle(default_conf, update, mocker) -> None: +async def test_reload_config_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._reload_config(update=update, context=MagicMock()) + await telegram._reload_config(update=update, context=MagicMock()) assert freqtradebot.state == State.RELOAD_CONFIG assert msg_mock.call_count == 1 assert 'Reloading config' in msg_mock.call_args_list[0][0][0] -def test_telegram_forceexit_handle(default_conf, update, ticker, fee, - ticker_sell_up, mocker) -> None: +async def test_telegram_forceexit_handle(default_conf, update, ticker, fee, + ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -978,7 +1033,7 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-2][0][0] @@ -1014,8 +1069,8 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, } == last_msg -def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, - ticker_sell_down, mocker) -> None: +async 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()) @@ -1050,7 +1105,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 4 @@ -1087,7 +1142,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, } == last_msg -def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -1113,7 +1168,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None # /forceexit all context = MagicMock() context.args = ["all"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # Called for each trade 2 times assert msg_mock.call_count == 8 @@ -1150,7 +1205,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None } == msg -def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: +async def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -1162,7 +1217,7 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] @@ -1172,12 +1227,12 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: # /forceexit 123456 context = MagicMock() context.args = ["123456"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 1 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: +async def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 4 mocker.patch.multiple( EXMS, @@ -1193,7 +1248,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: # /forceexit context = MagicMock() context.args = [] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # No pair assert msg_mock.call_args_list[0][1]['msg'] == 'No open trade found.' @@ -1202,7 +1257,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() # /forceexit - telegram._force_exit(update=update, context=context) + await 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 @@ -1210,9 +1265,9 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: assert keyboard[1][0].callback_data == 'force_exit__2 ' update = MagicMock() - update.callback_query = MagicMock() + update.callback_query = AsyncMock() update.callback_query.data = keyboard[1][0].callback_data - telegram._force_exit_inline(update, None) + await 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 @@ -1220,17 +1275,17 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: # Retry exiting - but cancel instead update.callback_query.reset_mock() - telegram._force_exit(update=update, context=context) + await 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) + await 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: +async def test_force_enter_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) @@ -1242,7 +1297,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # /forcelong ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1255,7 +1310,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # /forcelong ETH/BTC 0.055 context = MagicMock() context.args = ["ETH/BTC", "0.055"] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1263,20 +1318,19 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: assert fbuy_mock.call_args_list[0][0][1] == 0.055 -def test_force_enter_handle_exception(default_conf, update, mocker) -> None: +async def test_force_enter_handle_exception(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - update.message.text = '/forcebuy ETH/Nonepair' - telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) assert msg_mock.call_count == 1 assert msg_mock.call_args_list[0][0][0] == 'Force_entry not enabled.' -def test_force_enter_no_pair(default_conf, update, mocker) -> None: +async def test_force_enter_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) @@ -1288,7 +1342,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 0 assert msg_mock.call_count == 1 @@ -1298,13 +1352,13 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: # One additional button - cancel assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 update = MagicMock() - update.callback_query = MagicMock() + update.callback_query = AsyncMock() update.callback_query.data = 'XRP/USDT_||_long' - telegram._force_enter_inline(update, None) + await telegram._force_enter_inline(update, None) assert fbuy_mock.call_count == 1 -def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: +async def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, @@ -1316,13 +1370,13 @@ def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, moc # Create some test data create_mock_trades_usdt(fee) - telegram._performance(update=update, context=MagicMock()) + await telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'XRP/USDT\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] -def test_telegram_entry_tag_performance_handle( +async def test_telegram_entry_tag_performance_handle( default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, @@ -1335,26 +1389,26 @@ def test_telegram_entry_tag_performance_handle( create_mock_trades_usdt(fee) context = MagicMock() - telegram._enter_tag_performance(update=update, context=context) + await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0] assert 'TEST1\t3.987 USDT (5.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] - telegram._enter_tag_performance(update=update, context=context) + await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_enter_tag_performance', side_effect=RPCException('Error')) - telegram._enter_tag_performance(update=update, context=MagicMock()) + await telegram._enter_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee, - mocker) -> None: +async def test_telegram_exit_reason_performance_handle( + default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1366,26 +1420,26 @@ def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, tick create_mock_trades_usdt(fee) context = MagicMock() - telegram._exit_reason_performance(update=update, context=context) + await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0] assert 'roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] - telegram._exit_reason_performance(update=update, context=context) + await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_exit_reason_performance', side_effect=RPCException('Error')) - telegram._exit_reason_performance(update=update, context=MagicMock()) + await telegram._exit_reason_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, - mocker) -> None: +async def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, + mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1398,26 +1452,26 @@ def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, create_mock_trades_usdt(fee) context = MagicMock() - telegram._mix_tag_performance(update=update, context=context) + await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] assert ('TEST3 roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0]) context.args = ['XRP/USDT'] - telegram._mix_tag_performance(update=update, context=context) + await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_mix_tag_performance', side_effect=RPCException('Error')) - telegram._mix_tag_performance(update=update, context=MagicMock()) + await telegram._mix_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1427,7 +1481,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: patch_get_signal(freqtradebot) freqtradebot.state = State.STOPPED - telegram._count(update=update, context=MagicMock()) + await telegram._count(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -1436,7 +1490,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() msg_mock.reset_mock() - telegram._count(update=update, context=MagicMock()) + await telegram._count(update=update, context=MagicMock()) msg = ('
  current    max    total stake\n---------  -----  -------------\n'
            '        1      {}          {}
').format( @@ -1446,7 +1500,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg in msg_mock.call_args_list[0][0][0] -def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1454,7 +1508,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._locks(update=update, context=MagicMock()) + await telegram._locks(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No active locks.' in msg_mock.call_args_list[0][0][0] @@ -1463,7 +1517,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') - telegram._locks(update=update, context=MagicMock()) + await telegram._locks(update=update, context=MagicMock()) assert 'Pair' in msg_mock.call_args_list[0][0][0] assert 'Until' in msg_mock.call_args_list[0][0][0] @@ -1476,7 +1530,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None context = MagicMock() context.args = ['XRP/BTC'] msg_mock.reset_mock() - telegram._delete_locks(update=update, context=context) + await telegram._delete_locks(update=update, context=context) assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] assert 'randreason' in msg_mock.call_args_list[0][0][0] @@ -1484,11 +1538,11 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None assert 'deadbeef' not in msg_mock.call_args_list[0][0][0] -def test_whitelist_static(default_conf, update, mocker) -> None: +async def test_whitelist_static(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._whitelist(update=update, context=MagicMock()) + await telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1496,33 +1550,33 @@ def test_whitelist_static(default_conf, update, mocker) -> None: context = MagicMock() context.args = ['sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, NEO/BTC, XRP/BTC`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH, LTC, XRP, NEO`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly', 'sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH, LTC, NEO, XRP`" in msg_mock.call_args_list[0][0][0]) -def test_whitelist_dynamic(default_conf, update, mocker) -> None: +async def test_whitelist_dynamic(default_conf, update, mocker) -> None: mocker.patch(f'{EXMS}.exchange_has', return_value=True) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._whitelist(update=update, context=MagicMock()) + await telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1530,30 +1584,30 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: context = MagicMock() context.args = ['sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, NEO/BTC, XRP/BTC`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH, LTC, XRP, NEO`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly', 'sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH, LTC, NEO, XRP`" in msg_mock.call_args_list[0][0][0]) -def test_blacklist_static(default_conf, update, mocker) -> None: +async def test_blacklist_static(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._blacklist(update=update, context=MagicMock()) + await telegram._blacklist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1563,7 +1617,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: # /blacklist ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._blacklist(update=update, context=context) + await telegram._blacklist(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1572,7 +1626,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context = MagicMock() context.args = ["XRP/.*"] - telegram._blacklist(update=update, context=context) + await telegram._blacklist(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 4 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC, XRP/.*`" @@ -1581,13 +1635,13 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context.args = ["DOGE/BTC"] - telegram._blacklist_delete(update=update, context=context) + await telegram._blacklist_delete(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 3 pairs\n`HOT/BTC, ETH/BTC, XRP/.*`" in msg_mock.call_args_list[0][0][0]) -def test_telegram_logs(default_conf, update, mocker) -> None: +async def test_telegram_logs(default_conf, update, mocker) -> None: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), @@ -1598,13 +1652,13 @@ def test_telegram_logs(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 assert "freqtrade\\.rpc\\.telegram" in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() context.args = ["1"] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 msg_mock.reset_mock() @@ -1612,22 +1666,22 @@ def test_telegram_logs(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.MAX_MESSAGE_LENGTH', 200) context = MagicMock() context.args = [] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) # Called at least 2 times. Exact times will change with unrelated changes to setup messages # Therefore we don't test for this explicitly. assert msg_mock.call_count >= 2 -def test_edge_disabled(default_conf, update, mocker) -> None: +async def test_edge_disabled(default_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._edge(update=update, context=MagicMock()) + await telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0] -def test_edge_enabled(edge_conf, update, mocker) -> None: +async def test_edge_enabled(edge_conf, update, mocker) -> None: mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), @@ -1636,7 +1690,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, edge_conf) - telegram._edge(update=update, context=MagicMock()) + await telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
     assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
@@ -1645,7 +1699,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 
     mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
         return_value={}))
-    telegram._edge(update=update, context=MagicMock())
+    await telegram._edge(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert 'Edge only validated following pairs:' in msg_mock.call_args_list[0][0][0]
     assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
@@ -1654,20 +1708,20 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 @pytest.mark.parametrize('is_short,regex_pattern',
                          [(True, r"just now[ ]*XRP\/BTC \(#3\)  -1.00% \("),
                           (False, r"just now[ ]*XRP\/BTC \(#3\)  1.00% \(")])
-def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern):
+async def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
     context = MagicMock()
     context.args = []
 
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
 
     context.args = ['hello']
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
@@ -1676,7 +1730,7 @@ def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_patt
 
     context = MagicMock()
     context.args = [5]
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     msg_mock.call_count == 1
     assert "2 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
@@ -1686,13 +1740,13 @@ def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_patt
 
 
 @pytest.mark.parametrize('is_short', [True, False])
-def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
+async def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     context = MagicMock()
     context.args = []
 
-    telegram._delete_trade(update=update, context=context)
+    await telegram._delete_trade(update=update, context=context)
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1700,14 +1754,14 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
 
     context = MagicMock()
     context.args = [1]
-    telegram._delete_trade(update=update, context=context)
+    await telegram._delete_trade(update=update, context=context)
     msg_mock.call_count == 1
     assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
     assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
 
 
 @pytest.mark.parametrize('is_short', [True, False])
-def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
+async def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
 
     mocker.patch.multiple(
         EXMS,
@@ -1717,7 +1771,7 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
     context = MagicMock()
     context.args = []
 
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1725,7 +1779,7 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
 
     context = MagicMock()
     context.args = [5]
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert "No open order for trade_id" in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1734,43 +1788,43 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
     mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object())
     context = MagicMock()
     context.args = [6]
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert msg_mock.call_count == 1
     assert "Open order canceled." in msg_mock.call_args_list[0][0][0]
 
 
-def test_help_handle(default_conf, update, mocker) -> None:
+async def test_help_handle(default_conf, update, mocker) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._help(update=update, context=MagicMock())
+    await telegram._help(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
 
 
-def test_version_handle(default_conf, update, mocker) -> None:
+async def test_version_handle(default_conf, update, mocker) -> None:
 
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._version(update=update, context=MagicMock())
+    await telegram._version(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
     freqtradebot.strategy.version = lambda: '1.1.1'
 
-    telegram._version(update=update, context=MagicMock())
+    await telegram._version(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
     assert '*Strategy version: * `1.1.1`' in msg_mock.call_args_list[0][0][0]
 
 
-def test_show_config_handle(default_conf, update, mocker) -> None:
+async def test_show_config_handle(default_conf, update, mocker) -> None:
 
     default_conf['runmode'] = RunMode.DRY_RUN
 
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._show_config(update=update, context=MagicMock())
+    await telegram._show_config(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
     assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
@@ -1779,7 +1833,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
 
     msg_mock.reset_mock()
     freqtradebot.config['trailing_stop'] = True
-    telegram._show_config(update=update, context=MagicMock())
+    await telegram._show_config(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
     assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
@@ -2086,7 +2140,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
         telegram._rpc._fiat_converter.convert_amount = old_convamount
 
 
-def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
+async def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
@@ -2178,7 +2232,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
     assert msg_mock.call_args[0][0] == '*Status:* `running`'
 
 
-def test_warning_notification(default_conf, mocker) -> None:
+async def test_warning_notification(default_conf, mocker) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     telegram.send_msg({
         'type': RPCMessageType.WARNING,
@@ -2323,60 +2377,62 @@ def test__sell_emoji(default_conf, mocker, msg, expected):
     assert telegram._get_sell_emoji(msg) == expected
 
 
-def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
+async def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
+    bot.send_message = AsyncMock()
+    bot.edit_message_text = AsyncMock()
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
-    telegram._updater = MagicMock()
-    telegram._updater.bot = bot
+    telegram._app = MagicMock()
+    telegram._app.bot = bot
 
-    telegram._config['telegram']['enabled'] = True
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
     assert len(bot.method_calls) == 1
 
     # Test update
     query = MagicMock()
-    telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
-    edit_message_text = telegram._updater.bot.edit_message_text
+    await telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
+    edit_message_text = telegram._app.bot.edit_message_text
     assert edit_message_text.call_count == 1
     assert "Updated: " in edit_message_text.call_args_list[0][1]['text']
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("not modified"))
-    telegram._send_msg('test', callback_path="DeadBeef", query=query)
-    assert telegram._updater.bot.edit_message_text.call_count == 1
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("not modified"))
+    await telegram._send_msg('test', callback_path="DeadBeef", query=query)
+    assert telegram._app.bot.edit_message_text.call_count == 1
     assert not log_has_re(r"TelegramError: .*", caplog)
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest(""))
-    telegram._send_msg('test2', callback_path="DeadBeef", query=query)
-    assert telegram._updater.bot.edit_message_text.call_count == 1
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest(""))
+    await telegram._send_msg('test2', callback_path="DeadBeef", query=query)
+    assert telegram._app.bot.edit_message_text.call_count == 1
     assert log_has_re(r"TelegramError: .*", caplog)
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=TelegramError("DeadBEEF"))
-    telegram._send_msg('test3', callback_path="DeadBeef", query=query)
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=TelegramError("DeadBEEF"))
+    await telegram._send_msg('test3', callback_path="DeadBeef", query=query)
 
     assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog)
 
 
-def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
+async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
-    telegram._updater = MagicMock()
-    telegram._updater.bot = bot
+    telegram._app = MagicMock()
+    telegram._app.bot = bot
 
     telegram._config['telegram']['enabled'] = True
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
 
     # Bot should've tried to send it twice
     assert len(bot.method_calls) == 2
     assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog)
 
 
-def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
+@pytest.mark.filterwarnings("ignore:.*ChatPermissions")
+async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
-    bot.send_message = MagicMock()
+    bot.send_message = AsyncMock()
     freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     rpc = RPC(freqtradebot)
 
@@ -2392,14 +2448,14 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
 
     def init_telegram(freqtradebot):
         telegram = Telegram(rpc, default_conf)
-        telegram._updater = MagicMock()
-        telegram._updater.bot = bot
+        telegram._app = MagicMock()
+        telegram._app.bot = bot
         return telegram
 
     # no keyboard in config -> default keyboard
     freqtradebot.config['telegram']['enabled'] = True
     telegram = init_telegram(freqtradebot)
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
     used_keyboard = bot.send_message.call_args[1]['reply_markup']
     assert used_keyboard == default_keyboard
 
@@ -2416,7 +2472,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
     freqtradebot.config['telegram']['enabled'] = True
     freqtradebot.config['telegram']['keyboard'] = custom_keys_list
     telegram = init_telegram(freqtradebot)
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
     used_keyboard = bot.send_message.call_args[1]['reply_markup']
     assert used_keyboard == custom_keyboard
     assert log_has("using custom keyboard from config.json: "
@@ -2424,13 +2480,14 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
                    "'/start', '/reload_config', '/help']]", caplog)
 
 
-def test_change_market_direction(default_conf, mocker, update) -> None:
+async def test_change_market_direction(default_conf, mocker, update) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.NONE
     context = MagicMock()
     context.args = ["long"]
-    telegram._changemarketdir(update, context)
+    await telegram._changemarketdir(update, context)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG
     context = MagicMock()
     context.args = ["invalid"]
+    await telegram._changemarketdir(update, context)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG