diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 02c580ebd..c3e6f478f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -19,7 +19,6 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') - MAX_TELEGRAM_MESSAGE_LENGTH = 4096 @@ -29,6 +28,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: :param command_handler: Telegram CommandHandler :return: decorated function """ + def wrapper(self, *args, **kwargs): """ Decorator logic """ update = kwargs.get('update') or args[0] @@ -133,7 +133,7 @@ class Telegram(RPC): else: msg['stake_amount_fiat'] = 0 - message = ("*{exchange}:* Buying {pair}\n" + message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{limit:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" @@ -144,7 +144,8 @@ class Telegram(RPC): message += ")`" elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: - message = "*{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg) + 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) @@ -153,7 +154,9 @@ class Telegram(RPC): microsecond=0) - msg['open_date'].replace(microsecond=0) msg['duration_min'] = msg['duration'].total_seconds() / 60 - message = ("*{exchange}:* Selling {pair}\n" + msg['emoji'] = self._get_sell_emoji(msg) + + message = ("{emoji} *{exchange}:* Selling {pair}\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" @@ -165,21 +168,21 @@ class Telegram(RPC): # 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): + 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 = ("*{exchange}:* Cancelling Open Sell Order " + 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 = '*Warning:* `{status}`'.format(**msg) + message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION: message = '{status}'.format(**msg) @@ -189,6 +192,20 @@ class Telegram(RPC): self._send_msg(message) + def _get_sell_emoji(self, msg): + """ + Get emoji for sell-side + """ + + if float(msg['profit_percent']) >= 5.0: + return "\N{ROCKET}" + elif float(msg['profit_percent']) >= 0.0: + return "\N{EIGHT SPOKED ASTERISK}" + elif msg['sell_reason'] == "stop_loss": + return"\N{WARNING SIGN}" + else: + return "\N{CROSS MARK}" + @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ @@ -222,8 +239,8 @@ class Telegram(RPC): # 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 "", + 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}` " + @@ -368,14 +385,14 @@ class Telegram(RPC): "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) + "\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) @@ -592,7 +609,7 @@ class Telegram(RPC): "*/profit:* `Lists cumulative profit from all finished trades`\n" \ "*/forcesell |all:* `Instantly sells the given trade or all trades, " \ "regardless of profit`\n" \ - f"{forcebuy_text if self._config.get('forcebuy_enable', False) else '' }" \ + f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" \ "*/performance:* `Show performance of each finished trade grouped by pair`\n" \ "*/daily :* `Shows profit or loss per day, over the last n days`\n" \ "*/count:* `Show number of trades running compared to allowed number of trades`" \ diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 4895d67e4..21b9df81c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1225,7 +1225,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-1) }) assert msg_mock.call_args[0][0] \ - == '*Bittrex:* Buying ETH/BTC\n' \ + == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1247,7 +1247,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'pair': 'ETH/BTC', }) assert msg_mock.call_args[0][0] \ - == ('*Bittrex:* Cancelling Open Buy Order for ETH/BTC') + == ('\N{WARNING SIGN} *Bittrex:* Cancelling Open Buy Order for ETH/BTC') def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1280,7 +1280,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == ('*Binance:* Selling KEY/ETH\n' + == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1308,7 +1308,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == ('*Binance:* Selling KEY/ETH\n' + == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1338,7 +1338,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'Cancelled on exchange' }) assert msg_mock.call_args[0][0] \ - == ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. ' + 'Reason: Cancelled on exchange') msg_mock.reset_mock() telegram.send_msg({ @@ -1348,7 +1349,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'timeout' }) assert msg_mock.call_args[0][0] \ - == ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') # Reset singleton function to avoid random breaks telegram._fiat_converter.convert_amount = old_convamount @@ -1382,7 +1383,7 @@ def test_warning_notification(default_conf, mocker) -> None: 'type': RPCMessageType.WARNING_NOTIFICATION, 'status': 'message' }) - assert msg_mock.call_args[0][0] == '*Warning:* `message`' + assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' def test_custom_notification(default_conf, mocker) -> None: @@ -1441,7 +1442,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-1) }) assert msg_mock.call_args[0][0] \ - == '*Bittrex:* Buying ETH/BTC\n' \ + == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1477,7 +1478,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == '*Binance:* Selling KEY/ETH\n' \ + == '\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00007500`\n' \ '*Current Rate:* `0.00003201`\n' \ @@ -1487,6 +1488,29 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: '*Profit:* `-57.41%`' +@pytest.mark.parametrize('msg,expected', [ + ({'profit_percent': 20.1, 'sell_reason': 'roi'}, "\N{ROCKET}"), + ({'profit_percent': 5.1, 'sell_reason': 'roi'}, "\N{ROCKET}"), + ({'profit_percent': 2.56, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_percent': 1.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_percent': 0.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_percent': -5.0, 'sell_reason': 'stop_loss'}, "\N{WARNING SIGN}"), + ({'profit_percent': -2.0, 'sell_reason': 'sell_signal'}, "\N{CROSS MARK}"), +]) +def test__sell_emoji(default_conf, mocker, msg, expected): + del default_conf['fiat_display_currency'] + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + + assert telegram._get_sell_emoji(msg) == expected + + def test__send_msg(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock()