Merge pull request #3787 from freqtrade/rpc/telegram_settings
Allow Notification finetuning for telegram messages
This commit is contained in:
commit
66ca596e7c
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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']
|
||||||
},
|
},
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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()}'
|
||||||
})
|
})
|
||||||
|
@ -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(
|
||||||
|
@ -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()}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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`'
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user