diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py deleted file mode 100644 index cfe480bd9..000000000 --- a/freqtrade/rpc/telegram.py +++ /dev/null @@ -1,709 +0,0 @@ -# pragma pylint: disable=unused-argument, unused-variable, protected-access, invalid-name - -""" -This module manage Telegram communication -""" -import logging -from typing import Any, Callable, Dict - -from tabulate import tabulate -from telegram import ParseMode, ReplyKeyboardMarkup, Update -from telegram.error import NetworkError, TelegramError -from telegram.ext import CallbackContext, CommandHandler, Updater - -from freqtrade.__init__ import __version__ -from freqtrade.rpc import RPC, RPCException, RPCMessageType -from freqtrade.rpc.fiat_convert import CryptoToFiatConverter - -logger = logging.getLogger(__name__) - -logger.debug('Included module rpc.telegram ...') - - -MAX_TELEGRAM_MESSAGE_LENGTH = 4096 - - -def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: - """ - 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): - """ Decorator logic """ - update = kwargs.get('update') or args[0] - - # Reject unauthorized messages - chat_id = int(self._config['telegram']['chat_id']) - - if int(update.message.chat_id) != chat_id: - logger.info( - 'Rejected unauthorized message from: %s', - update.message.chat_id - ) - return wrapper - - logger.info( - 'Executing handler: %s for chat_id: %s', - command_handler.__name__, - chat_id - ) - try: - return command_handler(self, *args, **kwargs) - except BaseException: - logger.exception('Exception occurred within Telegram module') - - return wrapper - - -class Telegram(RPC): - """ This class handles all telegram communication """ - - def __init__(self, freqtrade) -> None: - """ - Init the Telegram call, and init the super class RPC - :param freqtrade: Instance of a freqtrade bot - :return: None - """ - super().__init__(freqtrade) - - self._updater: Updater = None - self._config = freqtrade.config - self._init() - if self._config.get('fiat_display_currency', None): - self._fiat_converter = CryptoToFiatConverter() - - def _init(self) -> None: - """ - Initializes this module with the given config, - registers all known command handlers - and starts polling for message updates - """ - self._updater = Updater(token=self._config['telegram']['token'], workers=0, - use_context=True) - - # Register command handler and start telegram message polling - handles = [ - CommandHandler('status', self._status), - CommandHandler('profit', self._profit), - CommandHandler('balance', self._balance), - CommandHandler('start', self._start), - CommandHandler('stop', self._stop), - CommandHandler('forcesell', self._forcesell), - CommandHandler('forcebuy', self._forcebuy), - CommandHandler('performance', self._performance), - CommandHandler('daily', self._daily), - CommandHandler('count', self._count), - CommandHandler('reload_conf', self._reload_conf), - CommandHandler('show_config', self._show_config), - CommandHandler('stopbuy', self._stopbuy), - CommandHandler('whitelist', self._whitelist), - CommandHandler('blacklist', self._blacklist), - CommandHandler('edge', self._edge), - CommandHandler('help', self._help), - CommandHandler('version', self._version), - ] - for handle in handles: - self._updater.dispatcher.add_handler(handle) - self._updater.start_polling( - clean=True, - bootstrap_retries=-1, - timeout=30, - read_latency=60, - ) - logger.info( - 'rpc.telegram is listening for following commands: %s', - [h.command for h in handles] - ) - - def cleanup(self) -> None: - """ - Stops all running telegram threads. - :return: None - """ - self._updater.stop() - - def send_msg(self, msg: Dict[str, Any]) -> None: - """ Send a message to telegram channel """ - - '🔵'.encode('ascii', 'namereplace') - '🚀'.encode('ascii', 'namereplace') - '✳'.encode('ascii', 'namereplace') - '❌'.encode('ascii', 'namereplace') - '⚠'.encode('ascii', 'namereplace') - - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: - if self._fiat_converter: - msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( - msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) - else: - msg['stake_amount_fiat'] = 0 - - message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{limit:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg) - - if msg.get('fiat_currency', None): - message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg) - message += ")`" - - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: - message = "\N{WARNING SIGN} *{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg) - - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: - msg['amount'] = round(msg['amount'], 8) - msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) - msg['duration'] = msg['close_date'].replace( - microsecond=0) - msg['open_date'].replace(microsecond=0) - msg['duration_min'] = msg['duration'].total_seconds() / 60 - - if float(msg['profit_percent']) > 5.0: - message = ("\N{ROCKET} *{exchange}:* Selling {pair}\n").format(**msg) - - elif float(msg['profit_percent']) >= 0.0: - message = "\N{EIGHT SPOKED ASTERISK} *{exchange}:* Selling {pair}\n" - - elif msg['sell_reason'] == "stop_loss": - message = ("\N{WARNING SIGN} *{exchange}:* Selling {pair}\n").format(**msg) - - else: - message = ("\N{CROSS MARK} *{exchange}:* Selling {pair}\n").format(**msg) - - message += ("*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) - - # Check if all sell properties are available. - # This might not be the case if the message origin is triggered by /forcesell - if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) - and self._fiat_converter): - msg['profit_fiat'] = self._fiat_converter.convert_amount( - msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - message += (' `({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) - - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: - message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " - "for {pair}. Reason: {reason}").format(**msg) - - elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: - message = '*Status:* `{status}`'.format(**msg) - - elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: - message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - - elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION: - message = '{status}'.format(**msg) - - else: - raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) - - self._send_msg(message) - - @authorized_only - def _status(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /status. - Returns the current TradeThread status - :param bot: telegram bot - :param update: message update - :return: None - """ - - if 'table' in context.args: - self._status_table(update, context) - return - - try: - results = self._rpc_trade_status() - - messages = [] - for r in results: - lines = [ - "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", - "*Current Pair:* {pair}", - "*Amount:* `{amount} ({stake_amount} {base_currency})`", - "*Open Rate:* `{open_rate:.8f}`", - "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", - "*Current Rate:* `{current_rate:.8f}`", - ("*Close Profit:* `{close_profit_pct}`" - if r['close_profit_pct'] is not None else ""), - "*Current Profit:* `{current_profit_pct:.2f}%`", - - # Adding initial stoploss only if it is different from stoploss - "*Initial Stoploss:* `{initial_stop_loss:.8f}` " + - ("`({initial_stop_loss_pct:.2f}%)`") if ( - r['stop_loss'] != r['initial_stop_loss'] - and r['initial_stop_loss_pct'] is not None) else "", - - # Adding stoploss and stoploss percentage only if it is not None - "*Stoploss:* `{stop_loss:.8f}` " + - ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), - ] - if r['open_order']: - if r['sell_order_status']: - lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") - else: - lines.append("*Open Order:* `{open_order}`") - - # Filter empty lines using list-comprehension - messages.append("\n".join([line for line in lines if line]).format(**r)) - - for msg in messages: - self._send_msg(msg) - - except RPCException as e: - self._send_msg(str(e)) - - @authorized_only - def _status_table(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /status table. - Returns the current TradeThread status in table format - :param bot: telegram bot - :param update: message update - :return: None - """ - try: - statlist, head = self._rpc_status_table(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) - message = tabulate(statlist, headers=head, tablefmt='simple') - self._send_msg(f"
{message}", parse_mode=ParseMode.HTML) - except RPCException as e: - self._send_msg(str(e)) - - @authorized_only - def _daily(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /daily
{stats_tab}' - self._send_msg(message, parse_mode=ParseMode.HTML) - except RPCException as e: - self._send_msg(str(e)) - - @authorized_only - def _profit(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /profit. - Returns a cumulative profit statistics. - :param bot: telegram bot - :param update: message update - :return: None - """ - stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config.get('fiat_display_currency', '') - - stats = self._rpc_trade_statistics( - stake_cur, - fiat_disp_cur) - profit_closed_coin = stats['profit_closed_coin'] - profit_closed_percent = stats['profit_closed_percent'] - profit_closed_fiat = stats['profit_closed_fiat'] - profit_all_coin = stats['profit_all_coin'] - profit_all_percent = stats['profit_all_percent'] - profit_all_fiat = stats['profit_all_fiat'] - trade_count = stats['trade_count'] - first_trade_date = stats['first_trade_date'] - latest_trade_date = stats['latest_trade_date'] - avg_duration = stats['avg_duration'] - best_pair = stats['best_pair'] - best_rate = stats['best_rate'] - if stats['trade_count'] == 0: - markdown_msg = 'No trades yet.' - else: - # Message to display - if stats['closed_trade_count'] > 0: - markdown_msg = ("*ROI:* Closed trades\n" - f"∙ `{profit_closed_coin:.8f} {stake_cur} " - f"({profit_closed_percent:.2f}%)`\n" - f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n") - else: - markdown_msg = "`No closed trade` \n" - - markdown_msg += (f"*ROI:* All trades\n" - f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" - f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" - f"*Total Trade Count:* `{trade_count}`\n" - f"*First Trade opened:* `{first_trade_date}`\n" - f"*Latest Trade opened:* `{latest_trade_date}`") - if stats['closed_trade_count'] > 0: - markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" - f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") - self._send_msg(markdown_msg) - - @authorized_only - def _balance(self, update: Update, context: CallbackContext) -> None: - """ Handler for /balance """ - try: - result = self._rpc_balance(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) - - output = '' - if self._config['dry_run']: - output += ( - f"*Warning:* Simulated balances in Dry Mode.\n" - "This mode is still experimental!\n" - "Starting capital: " - f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" - ) - for currency in result['currencies']: - if currency['est_stake'] > 0.0001: - curr_output = "*{currency}:*\n" \ - "\t`Available: {free: .8f}`\n" \ - "\t`Balance: {balance: .8f}`\n" \ - "\t`Pending: {used: .8f}`\n" \ - "\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency) - else: - curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) - - # Handle overflowing messsage length - if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: - self._send_msg(output) - output = curr_output - else: - output += curr_output - - output += "\n*Estimated Value*:\n" \ - "\t`{stake}: {total: .8f}`\n" \ - "\t`{symbol}: {value: .2f}`\n".format(**result) - self._send_msg(output) - except RPCException as e: - self._send_msg(str(e)) - - @authorized_only - def _start(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /start. - Starts TradeThread - :param bot: telegram bot - :param update: message update - :return: None - """ - msg = self._rpc_start() - self._send_msg('Status: `{status}`'.format(**msg)) - - @authorized_only - def _stop(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /stop. - Stops TradeThread - :param bot: telegram bot - :param update: message update - :return: None - """ - msg = self._rpc_stop() - self._send_msg('Status: `{status}`'.format(**msg)) - - @authorized_only - def _reload_conf(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /reload_conf. - Triggers a config file reload - :param bot: telegram bot - :param update: message update - :return: None - """ - msg = self._rpc_reload_conf() - self._send_msg('Status: `{status}`'.format(**msg)) - - @authorized_only - def _stopbuy(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /stop_buy. - Sets max_open_trades to 0 and gracefully sells all open trades - :param bot: telegram bot - :param update: message update - :return: None - """ - msg = self._rpc_stopbuy() - self._send_msg('Status: `{status}`'.format(**msg)) - - @authorized_only - def _forcesell(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /forcesell
{pair}\t{profit:.2f}% ({count})
'.format(
- index=i + 1,
- pair=trade['pair'],
- profit=trade['profit'],
- count=trade['count']
- ) for i, trade in enumerate(trades))
- message = 'Performance:\n{}'.format(stats)
- self._send_msg(message, parse_mode=ParseMode.HTML)
- except RPCException as e:
- self._send_msg(str(e))
-
- @authorized_only
- def _count(self, update: Update, context: CallbackContext) -> None:
- """
- Handler for /count.
- Returns the number of trades running
- :param bot: telegram bot
- :param update: message update
- :return: None
- """
- try:
- counts = self._rpc_count()
- message = tabulate({k: [v] for k, v in counts.items()},
- headers=['current', 'max', 'total stake'],
- tablefmt='simple')
- message = "{}".format(message) - logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) - except RPCException as e: - self._send_msg(str(e)) - - @authorized_only - def _whitelist(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /whitelist - Shows the currently active whitelist - """ - try: - whitelist = self._rpc_whitelist() - - message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" - message += f"`{', '.join(whitelist['whitelist'])}`" - - logger.debug(message) - self._send_msg(message) - except RPCException as e: - self._send_msg(str(e)) - - @authorized_only - def _blacklist(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /blacklist - Shows the currently active blacklist - """ - try: - - blacklist = self._rpc_blacklist(context.args) - errmsgs = [] - for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") - if errmsgs: - 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) - except RPCException as e: - self._send_msg(str(e)) - - @authorized_only - def _edge(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /edge - Shows information related to Edge - """ - try: - edge_pairs = self._rpc_edge() - edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') - message = f'Edge only validated following pairs:\n
{edge_pairs_tab}' - self._send_msg(message, parse_mode=ParseMode.HTML) - except RPCException as e: - self._send_msg(str(e)) - - @authorized_only - def _help(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /help. - Show commands of the bot - :param bot: telegram bot - :param update: message update - :return: None - """ - forcebuy_text = "*/forcebuy