Merge Pull request #6919 into develop
This commit is contained in:
commit
9c65fad73f
BIN
docs/assets/discord_notification.png
Normal file
BIN
docs/assets/discord_notification.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
@ -239,3 +239,52 @@ Possible parameters are:
|
|||||||
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
||||||
|
|
||||||
The only possible value here is `{status}`.
|
The only possible value here is `{status}`.
|
||||||
|
|
||||||
|
## Discord
|
||||||
|
|
||||||
|
A special form of webhooks is available for discord.
|
||||||
|
You can configure this as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"discord": {
|
||||||
|
"enabled": true,
|
||||||
|
"webhook_url": "https://discord.com/api/webhooks/<Your webhook URL ...>",
|
||||||
|
"exit_fill": [
|
||||||
|
{"Trade ID": "{trade_id}"},
|
||||||
|
{"Exchange": "{exchange}"},
|
||||||
|
{"Pair": "{pair}"},
|
||||||
|
{"Direction": "{direction}"},
|
||||||
|
{"Open rate": "{open_rate}"},
|
||||||
|
{"Close rate": "{close_rate}"},
|
||||||
|
{"Amount": "{amount}"},
|
||||||
|
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Profit": "{profit_amount} {stake_currency}"},
|
||||||
|
{"Profitability": "{profit_ratio:.2%}"},
|
||||||
|
{"Enter tag": "{enter_tag}"},
|
||||||
|
{"Exit Reason": "{exit_reason}"},
|
||||||
|
{"Strategy": "{strategy}"},
|
||||||
|
{"Timeframe": "{timeframe}"},
|
||||||
|
],
|
||||||
|
"entry_fill": [
|
||||||
|
{"Trade ID": "{trade_id}"},
|
||||||
|
{"Exchange": "{exchange}"},
|
||||||
|
{"Pair": "{pair}"},
|
||||||
|
{"Direction": "{direction}"},
|
||||||
|
{"Open rate": "{open_rate}"},
|
||||||
|
{"Amount": "{amount}"},
|
||||||
|
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Enter tag": "{enter_tag}"},
|
||||||
|
{"Strategy": "{strategy} {timeframe}"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
||||||
|
|
||||||
|
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
||||||
|
|
||||||
|
The notifications will look as follows by default.
|
||||||
|
|
||||||
|
![discord-notification](assets/discord_notification.png)
|
||||||
|
@ -336,6 +336,47 @@ CONF_SCHEMA = {
|
|||||||
'webhookstatus': {'type': 'object'},
|
'webhookstatus': {'type': 'object'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'discord': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'webhook_url': {'type': 'string'},
|
||||||
|
"exit_fill": {
|
||||||
|
'type': 'array', 'items': {'type': 'object'},
|
||||||
|
'default': [
|
||||||
|
{"Trade ID": "{trade_id}"},
|
||||||
|
{"Exchange": "{exchange}"},
|
||||||
|
{"Pair": "{pair}"},
|
||||||
|
{"Direction": "{direction}"},
|
||||||
|
{"Open rate": "{open_rate}"},
|
||||||
|
{"Close rate": "{close_rate}"},
|
||||||
|
{"Amount": "{amount}"},
|
||||||
|
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Profit": "{profit_amount} {stake_currency}"},
|
||||||
|
{"Profitability": "{profit_ratio:.2%}"},
|
||||||
|
{"Enter tag": "{enter_tag}"},
|
||||||
|
{"Exit Reason": "{exit_reason}"},
|
||||||
|
{"Strategy": "{strategy}"},
|
||||||
|
{"Timeframe": "{timeframe}"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry_fill": {
|
||||||
|
'type': 'array', 'items': {'type': 'object'},
|
||||||
|
'default': [
|
||||||
|
{"Trade ID": "{trade_id}"},
|
||||||
|
{"Exchange": "{exchange}"},
|
||||||
|
{"Pair": "{pair}"},
|
||||||
|
{"Direction": "{direction}"},
|
||||||
|
{"Open rate": "{open_rate}"},
|
||||||
|
{"Amount": "{amount}"},
|
||||||
|
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Enter tag": "{enter_tag}"},
|
||||||
|
{"Strategy": "{strategy} {timeframe}"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
'api_server': {
|
'api_server': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
59
freqtrade/rpc/discord.py
Normal file
59
freqtrade/rpc/discord.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||||
|
from freqtrade.rpc import RPC
|
||||||
|
from freqtrade.rpc.webhook import Webhook
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Discord(Webhook):
|
||||||
|
def __init__(self, rpc: 'RPC', config: Dict[str, Any]):
|
||||||
|
# super().__init__(rpc, config)
|
||||||
|
self.rpc = rpc
|
||||||
|
self.config = config
|
||||||
|
self.strategy = config.get('strategy', '')
|
||||||
|
self.timeframe = config.get('timeframe', '')
|
||||||
|
|
||||||
|
self._url = self.config['discord']['webhook_url']
|
||||||
|
self._format = 'json'
|
||||||
|
self._retries = 1
|
||||||
|
self._retry_delay = 0.1
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""
|
||||||
|
Cleanup pending module resources.
|
||||||
|
This will do nothing for webhooks, they will simply not be called anymore
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_msg(self, msg) -> None:
|
||||||
|
logger.info(f"Sending discord message: {msg}")
|
||||||
|
|
||||||
|
if msg['type'].value in self.config['discord']:
|
||||||
|
|
||||||
|
msg['strategy'] = self.strategy
|
||||||
|
msg['timeframe'] = self.timeframe
|
||||||
|
fields = self.config['discord'].get(msg['type'].value)
|
||||||
|
color = 0x0000FF
|
||||||
|
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
|
||||||
|
profit_ratio = msg.get('profit_ratio')
|
||||||
|
color = (0x00FF00 if profit_ratio > 0 else 0xFF0000)
|
||||||
|
|
||||||
|
embeds = [{
|
||||||
|
'title': f"Trade: {msg['pair']} {msg['type'].value}",
|
||||||
|
'color': color,
|
||||||
|
'fields': [],
|
||||||
|
|
||||||
|
}]
|
||||||
|
for f in fields:
|
||||||
|
for k, v in f.items():
|
||||||
|
v = v.format(**msg)
|
||||||
|
embeds[0]['fields'].append( # type: ignore
|
||||||
|
{'name': k, 'value': v, 'inline': True})
|
||||||
|
|
||||||
|
# Send the message to discord channel
|
||||||
|
payload = {'embeds': embeds}
|
||||||
|
self._send_msg(payload)
|
@ -27,6 +27,12 @@ class RPCManager:
|
|||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc.telegram import Telegram
|
||||||
self.registered_modules.append(Telegram(self._rpc, config))
|
self.registered_modules.append(Telegram(self._rpc, config))
|
||||||
|
|
||||||
|
# Enable discord
|
||||||
|
if config.get('discord', {}).get('enabled', False):
|
||||||
|
logger.info('Enabling rpc.discord ...')
|
||||||
|
from freqtrade.rpc.discord import Discord
|
||||||
|
self.registered_modules.append(Discord(self._rpc, config))
|
||||||
|
|
||||||
# Enable Webhook
|
# Enable Webhook
|
||||||
if config.get('webhook', {}).get('enabled', False):
|
if config.get('webhook', {}).get('enabled', False):
|
||||||
logger.info('Enabling rpc.webhook ...')
|
logger.info('Enabling rpc.webhook ...')
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -7,6 +8,7 @@ from requests import RequestException
|
|||||||
|
|
||||||
from freqtrade.enums import ExitType, RPCMessageType
|
from freqtrade.enums import ExitType, RPCMessageType
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
|
from freqtrade.rpc.discord import Discord
|
||||||
from freqtrade.rpc.webhook import Webhook
|
from freqtrade.rpc.webhook import Webhook
|
||||||
from tests.conftest import get_patched_freqtradebot, log_has
|
from tests.conftest import get_patched_freqtradebot, log_has
|
||||||
|
|
||||||
@ -406,3 +408,42 @@ def test__send_msg_with_raw_format(default_conf, mocker, caplog):
|
|||||||
webhook._send_msg(msg)
|
webhook._send_msg(msg)
|
||||||
|
|
||||||
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
|
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_discord(default_conf, mocker):
|
||||||
|
|
||||||
|
default_conf["discord"] = {
|
||||||
|
'enabled': True,
|
||||||
|
'webhook_url': "https://webhookurl..."
|
||||||
|
}
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
|
discord = Discord(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||||
|
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.EXIT_FILL,
|
||||||
|
'trade_id': 1,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'direction': 'Long',
|
||||||
|
'gain': "profit",
|
||||||
|
'close_rate': 0.005,
|
||||||
|
'amount': 0.8,
|
||||||
|
'order_type': 'limit',
|
||||||
|
'open_date': datetime.now() - timedelta(days=1),
|
||||||
|
'close_date': datetime.now(),
|
||||||
|
'open_rate': 0.004,
|
||||||
|
'current_rate': 0.005,
|
||||||
|
'profit_amount': 0.001,
|
||||||
|
'profit_ratio': 0.20,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'enter_tag': 'enter_tagggg',
|
||||||
|
'exit_reason': ExitType.STOP_LOSS.value,
|
||||||
|
}
|
||||||
|
discord.send_msg(msg=msg)
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'embeds' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert 'title' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||||
|
assert 'color' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||||
|
assert 'fields' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||||
|
Loading…
Reference in New Issue
Block a user