Merge pull request #3787 from freqtrade/rpc/telegram_settings

Allow Notification finetuning for telegram messages
This commit is contained in:
Matthias 2020-09-23 10:20:43 +02:00 committed by GitHub
commit 66ca596e7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 103 additions and 23 deletions

View File

@ -116,7 +116,16 @@
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "your_telegram_token", "token": "your_telegram_token",
"chat_id": "your_telegram_chat_id" "chat_id": "your_telegram_chat_id",
"notification_settings": {
"status": "on",
"warning": "on",
"startup": "on",
"buy": "on",
"sell": "on",
"buy_cancel": "on",
"sell_cancel": "on"
}
}, },
"api_server": { "api_server": {
"enabled": false, "enabled": false,

View File

@ -41,6 +41,34 @@ Talk to the [userinfobot](https://telegram.me/userinfobot)
Get your "Id", you will use it for the config parameter `chat_id`. Get your "Id", you will use it for the config parameter `chat_id`.
## Control telegram noise
Freqtrade provides means to control the verbosity of your telegram bot.
Each setting has the following possible values:
* `on` - Messages will be sent, and user will be notified.
* `silent` - Message will be sent, Notification will be without sound / vibration.
* `off` - Skip sending a message-type all together.
Example configuration showing the different settings:
``` json
"telegram": {
"enabled": true,
"token": "your_telegram_token",
"chat_id": "your_telegram_chat_id",
"notification_settings": {
"status": "silent",
"warning": "on",
"startup": "off",
"buy": "silent",
"sell": "on",
"buy_cancel": "silent",
"sell_cancel": "on"
}
},
```
## Telegram commands ## Telegram commands
Per default, the Telegram bot shows predefined commands. Some commands Per default, the Telegram bot shows predefined commands. Some commands

View File

@ -39,6 +39,8 @@ USERPATH_HYPEROPTS = 'hyperopts'
USERPATH_STRATEGIES = 'strategies' USERPATH_STRATEGIES = 'strategies'
USERPATH_NOTEBOOKS = 'notebooks' USERPATH_NOTEBOOKS = 'notebooks'
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
# Soure files with destination directories within user-directory # Soure files with destination directories within user-directory
USER_DATA_FILES = { USER_DATA_FILES = {
'sample_strategy.py': USERPATH_STRATEGIES, 'sample_strategy.py': USERPATH_STRATEGIES,
@ -201,6 +203,18 @@ CONF_SCHEMA = {
'enabled': {'type': 'boolean'}, 'enabled': {'type': 'boolean'},
'token': {'type': 'string'}, 'token': {'type': 'string'},
'chat_id': {'type': 'string'}, 'chat_id': {'type': 'string'},
'notification_settings': {
'type': 'object',
'properties': {
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}
}
}
}, },
'required': ['enabled', 'token', 'chat_id'] 'required': ['enabled', 'token', 'chat_id']
}, },

View File

@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
class RPCMessageType(Enum): class RPCMessageType(Enum):
STATUS_NOTIFICATION = 'status' STATUS_NOTIFICATION = 'status'
WARNING_NOTIFICATION = 'warning' WARNING_NOTIFICATION = 'warning'
CUSTOM_NOTIFICATION = 'custom' STARTUP_NOTIFICATION = 'startup'
BUY_NOTIFICATION = 'buy' BUY_NOTIFICATION = 'buy'
BUY_CANCEL_NOTIFICATION = 'buy_cancel' BUY_CANCEL_NOTIFICATION = 'buy_cancel'
SELL_NOTIFICATION = 'sell' SELL_NOTIFICATION = 'sell'
@ -36,6 +36,9 @@ class RPCMessageType(Enum):
def __repr__(self): def __repr__(self):
return self.value return self.value
def __str__(self):
return self.value
class RPCException(Exception): class RPCException(Exception):
""" """

View File

@ -59,7 +59,7 @@ class RPCManager:
try: try:
mod.send_msg(msg) mod.send_msg(msg)
except NotImplementedError: except NotImplementedError:
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.") logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.")
def startup_messages(self, config: Dict[str, Any], pairlist) -> None: def startup_messages(self, config: Dict[str, Any], pairlist) -> None:
if config['dry_run']: if config['dry_run']:
@ -76,7 +76,7 @@ class RPCManager:
exchange_name = config['exchange']['name'] exchange_name = config['exchange']['name']
strategy_name = config.get('strategy', '') strategy_name = config.get('strategy', '')
self.send_msg({ self.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION, 'type': RPCMessageType.STARTUP_NOTIFICATION,
'status': f'*Exchange:* `{exchange_name}`\n' 'status': f'*Exchange:* `{exchange_name}`\n'
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
f'*Minimum ROI:* `{minimal_roi}`\n' f'*Minimum ROI:* `{minimal_roi}`\n'
@ -85,7 +85,7 @@ class RPCManager:
f'*Strategy:* `{strategy_name}`' f'*Strategy:* `{strategy_name}`'
}) })
self.send_msg({ self.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION, 'type': RPCMessageType.STARTUP_NOTIFICATION,
'status': f'Searching for {stake_currency} pairs to buy and sell ' 'status': f'Searching for {stake_currency} pairs to buy and sell '
f'based on {pairlist.short_desc()}' f'based on {pairlist.short_desc()}'
}) })

View File

@ -132,6 +132,13 @@ class Telegram(RPC):
def send_msg(self, msg: Dict[str, Any]) -> None: def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """ """ Send a message to telegram channel """
noti = self._config['telegram'].get('notification_settings', {}
).get(str(msg['type']), 'on')
if noti == 'off':
logger.info(f"Notification '{msg['type']}' not sent.")
# Notification disabled
return
if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
if self._fiat_converter: if self._fiat_converter:
msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( msg['stake_amount_fiat'] = self._fiat_converter.convert_amount(
@ -190,13 +197,13 @@ class Telegram(RPC):
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION: elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION:
message = '{status}'.format(**msg) message = '{status}'.format(**msg)
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
self._send_msg(message) self._send_msg(message, disable_notification=(noti == 'silent'))
def _get_sell_emoji(self, msg): def _get_sell_emoji(self, msg):
""" """
@ -773,7 +780,8 @@ class Telegram(RPC):
f"*Current state:* `{val['state']}`" f"*Current state:* `{val['state']}`"
) )
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN,
disable_notification: bool = False) -> None:
""" """
Send given markdown message Send given markdown message
:param msg: message :param msg: message
@ -794,7 +802,8 @@ class Telegram(RPC):
self._config['telegram']['chat_id'], self._config['telegram']['chat_id'],
text=msg, text=msg,
parse_mode=parse_mode, parse_mode=parse_mode,
reply_markup=reply_markup reply_markup=reply_markup,
disable_notification=disable_notification,
) )
except NetworkError as network_err: except NetworkError as network_err:
# Sometimes the telegram server resets the current connection, # Sometimes the telegram server resets the current connection,
@ -807,7 +816,8 @@ class Telegram(RPC):
self._config['telegram']['chat_id'], self._config['telegram']['chat_id'],
text=msg, text=msg,
parse_mode=parse_mode, parse_mode=parse_mode,
reply_markup=reply_markup reply_markup=reply_markup,
disable_notification=disable_notification,
) )
except TelegramError as telegram_err: except TelegramError as telegram_err:
logger.warning( logger.warning(

View File

@ -48,13 +48,13 @@ class Webhook(RPC):
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksellcancel', None) valuedict = self._config['webhook'].get('webhooksellcancel', None)
elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.CUSTOM_NOTIFICATION, RPCMessageType.STARTUP_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION): RPCMessageType.WARNING_NOTIFICATION):
valuedict = self._config['webhook'].get('webhookstatus', None) valuedict = self._config['webhook'].get('webhookstatus', None)
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
if not valuedict: if not valuedict:
logger.info("Message type %s not configured for webhooks", msg['type']) logger.info("Message type '%s' not configured for webhooks", msg['type'])
return return
payload = {key: value.format(**msg) for (key, value) in valuedict.items()} payload = {key: value.format(**msg) for (key, value) in valuedict.items()}

View File

@ -124,10 +124,10 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.send_msg({'type': RPCMessageType.CUSTOM_NOTIFICATION, rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION,
'status': 'TestMessage'}) 'status': 'TestMessage'})
assert log_has( assert log_has(
"Message type RPCMessageType.CUSTOM_NOTIFICATION not implemented by handler webhook.", "Message type 'startup' not implemented by handler webhook.",
caplog) caplog)

View File

@ -1299,16 +1299,14 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0]
def test_send_msg_buy_notification(default_conf, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
_send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) msg = {
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.BUY_NOTIFICATION, 'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -1321,7 +1319,10 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
'current_rate': 1.099e-05, 'current_rate': 1.099e-05,
'amount': 1333.3333333333335, 'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1) 'open_date': arrow.utcnow().shift(hours=-1)
}) }
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg(msg)
assert msg_mock.call_args[0][0] \ assert msg_mock.call_args[0][0] \
== '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \
'*Amount:* `1333.33333333`\n' \ '*Amount:* `1333.33333333`\n' \
@ -1329,6 +1330,21 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
'*Current Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \
'*Total:* `(0.001000 BTC, 12.345 USD)`' '*Total:* `(0.001000 BTC, 12.345 USD)`'
freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'}
caplog.clear()
msg_mock.reset_mock()
telegram.send_msg(msg)
msg_mock.call_count == 0
log_has("Notification 'buy' not sent.", caplog)
freqtradebot.config['telegram']['notification_settings'] = {'buy': 'silent'}
caplog.clear()
msg_mock.reset_mock()
telegram.send_msg(msg)
msg_mock.call_count == 1
msg_mock.call_args_list[0][1]['disable_notification'] is True
def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
msg_mock = MagicMock() msg_mock = MagicMock()
@ -1485,7 +1501,7 @@ def test_warning_notification(default_conf, mocker) -> None:
assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`'
def test_custom_notification(default_conf, mocker) -> None: def test_startup_notification(default_conf, mocker) -> None:
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
@ -1495,7 +1511,7 @@ def test_custom_notification(default_conf, mocker) -> None:
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION, 'type': RPCMessageType.STARTUP_NOTIFICATION,
'status': '*Custom:* `Hello World`' 'status': '*Custom:* `Hello World`'
}) })
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'

View File

@ -150,7 +150,7 @@ def test_send_msg(default_conf, mocker):
default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg))
for msgtype in [RPCMessageType.STATUS_NOTIFICATION, for msgtype in [RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION,
RPCMessageType.CUSTOM_NOTIFICATION]: RPCMessageType.STARTUP_NOTIFICATION]:
# Test notification # Test notification
msg = { msg = {
'type': msgtype, 'type': msgtype,
@ -174,7 +174,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks", assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks",
caplog) caplog)
default_conf["webhook"] = get_webhook_dict() default_conf["webhook"] = get_webhook_dict()