# pragma pylint: disable=missing-docstring, C0103, protected-access from datetime import datetime, timedelta from unittest.mock import MagicMock from requests import RequestException from freqtrade.enums import ExitType, RPCMessageType from freqtrade.rpc import RPC from freqtrade.rpc.discord import Discord from freqtrade.rpc.webhook import Webhook from tests.conftest import get_patched_freqtradebot, log_has def get_webhook_dict() -> dict: return { "enabled": True, "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/", "webhookentry": { "value1": "Buying {pair}", "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}", "value4": "leverage {leverage:.1f}", "value5": "direction {direction}" }, "webhookentrycancel": { "value1": "Cancelling Open Buy Order for {pair}", "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}", "value4": "leverage {leverage:.1f}", "value5": "direction {direction}" }, "webhookentryfill": { "value1": "Buy Order for {pair} filled", "value2": "at {open_rate:8f}", "value3": "{stake_amount:8f} {stake_currency}", "value4": "leverage {leverage:.1f}", "value5": "direction {direction}" }, "webhookexit": { "value1": "Selling {pair}", "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, "webhookexitcancel": { "value1": "Cancelling Open Sell Order for {pair}", "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, "webhookexitfill": { "value1": "Sell Order for {pair} filled", "value2": "at {close_rate:8f}", "value3": "" }, "webhookstatus": { "value1": "Status: {status}", "value2": "", "value3": "" } } def test__init__(mocker, default_conf): default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"} webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert webhook._config == default_conf def test_send_msg_webhook(default_conf, mocker): default_conf["webhook"] = get_webhook_dict() msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) # Test buy msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.ENTRY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'leverage': 1.0, 'direction': 'Long', 'limit': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, 'stake_currency': 'BTC', 'fiat_currency': 'EUR' } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookentry"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookentry"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookentry"]["value3"].format(**msg)) assert (msg_mock.call_args[0][0]["value4"] == default_conf["webhook"]["webhookentry"]["value4"].format(**msg)) assert (msg_mock.call_args[0][0]["value5"] == default_conf["webhook"]["webhookentry"]["value5"].format(**msg)) # Test short msg_mock.reset_mock() msg = { 'type': RPCMessageType.ENTRY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'leverage': 2.0, 'direction': 'Short', 'limit': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, 'stake_currency': 'BTC', 'fiat_currency': 'EUR' } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookentry"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookentry"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookentry"]["value3"].format(**msg)) assert (msg_mock.call_args[0][0]["value4"] == default_conf["webhook"]["webhookentry"]["value4"].format(**msg)) assert (msg_mock.call_args[0][0]["value5"] == default_conf["webhook"]["webhookentry"]["value5"].format(**msg)) # Test buy cancel msg_mock.reset_mock() msg = { 'type': RPCMessageType.ENTRY_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'leverage': 1.0, 'direction': 'Long', 'limit': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, 'stake_currency': 'BTC', 'fiat_currency': 'EUR' } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookentrycancel"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookentrycancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookentrycancel"]["value3"].format(**msg)) # Test short cancel msg_mock.reset_mock() msg = { 'type': RPCMessageType.ENTRY_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'leverage': 2.0, 'direction': 'Short', 'limit': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, 'stake_currency': 'BTC', 'fiat_currency': 'EUR' } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookentrycancel"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookentrycancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookentrycancel"]["value3"].format(**msg)) assert (msg_mock.call_args[0][0]["value4"] == default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg)) assert (msg_mock.call_args[0][0]["value5"] == default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg)) # Test buy fill msg_mock.reset_mock() msg = { 'type': RPCMessageType.ENTRY_FILL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'leverage': 1.0, 'direction': 'Long', 'open_rate': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, 'stake_currency': 'BTC', 'fiat_currency': 'EUR' } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookentryfill"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookentryfill"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookentryfill"]["value3"].format(**msg)) assert (msg_mock.call_args[0][0]["value4"] == default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg)) assert (msg_mock.call_args[0][0]["value5"] == default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg)) # Test short fill msg_mock.reset_mock() msg = { 'type': RPCMessageType.ENTRY_FILL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'leverage': 2.0, 'direction': 'Short', 'open_rate': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, 'stake_currency': 'BTC', 'fiat_currency': 'EUR' } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookentryfill"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookentryfill"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookentryfill"]["value3"].format(**msg)) assert (msg_mock.call_args[0][0]["value4"] == default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg)) assert (msg_mock.call_args[0][0]["value5"] == default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg)) # Test sell msg_mock.reset_mock() msg = { 'type': RPCMessageType.EXIT, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, 'amount': 0.8, 'order_type': 'limit', 'open_rate': 0.004, 'current_rate': 0.005, 'profit_amount': 0.001, 'profit_ratio': 0.20, 'stake_currency': 'BTC', 'sell_reason': ExitType.STOP_LOSS.value } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookexit"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookexit"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookexit"]["value3"].format(**msg)) # Test sell cancel msg_mock.reset_mock() msg = { 'type': RPCMessageType.EXIT_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, 'amount': 0.8, 'order_type': 'limit', 'open_rate': 0.004, 'current_rate': 0.005, 'profit_amount': 0.001, 'profit_ratio': 0.20, 'stake_currency': 'BTC', 'sell_reason': ExitType.STOP_LOSS.value } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookexitcancel"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookexitcancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookexitcancel"]["value3"].format(**msg)) # Test Sell fill msg_mock.reset_mock() msg = { 'type': RPCMessageType.EXIT_FILL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'close_rate': 0.005, 'amount': 0.8, 'order_type': 'limit', 'open_rate': 0.004, 'current_rate': 0.005, 'profit_amount': 0.001, 'profit_ratio': 0.20, 'stake_currency': 'BTC', 'sell_reason': ExitType.STOP_LOSS.value } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookexitfill"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookexitfill"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookexitfill"]["value3"].format(**msg)) for msgtype in [RPCMessageType.STATUS, RPCMessageType.WARNING, RPCMessageType.STARTUP]: # Test notification msg = { 'type': msgtype, 'status': 'Unfilled sell order for BTC cancelled due to timeout' } msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) webhook.send_msg(msg) assert msg_mock.call_count == 1 assert (msg_mock.call_args[0][0]["value1"] == default_conf["webhook"]["webhookstatus"]["value1"].format(**msg)) assert (msg_mock.call_args[0][0]["value2"] == default_conf["webhook"]["webhookstatus"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookstatus"]["value3"].format(**msg)) def test_exception_send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() del default_conf["webhook"]["webhookentry"] webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) webhook.send_msg({'type': RPCMessageType.ENTRY}) assert log_has(f"Message type '{RPCMessageType.ENTRY}' not configured for webhooks", caplog) default_conf["webhook"] = get_webhook_dict() default_conf["webhook"]["strategy_msg"] = {"value1": "{DEADBEEF:8f}"} msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { 'type': RPCMessageType.STRATEGY_MSG, 'msg': 'hello world', } webhook.send_msg(msg) assert log_has("Problem calling Webhook. Please check your webhook configuration. " "Exception: 'DEADBEEF'", caplog) # Test no failure for not implemented but known messagetypes for e in RPCMessageType: msg = { 'type': e, 'status': 'whatever' } webhook.send_msg(msg) # Test no failure for not implemented but known messagetypes for e in RPCMessageType: msg = { 'type': e, 'status': 'whatever' } webhook.send_msg(msg) def test__send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = {'value1': 'DEADBEEF', 'value2': 'ALIVEBEEF', 'value3': 'FREQTRADE'} post = MagicMock() mocker.patch("freqtrade.rpc.webhook.post", post) webhook._send_msg(msg) assert post.call_count == 1 assert post.call_args[1] == {'data': msg} assert post.call_args[0] == (default_conf['webhook']['url'], ) post = MagicMock(side_effect=RequestException) mocker.patch("freqtrade.rpc.webhook.post", post) webhook._send_msg(msg) assert log_has('Could not call webhook url. Exception: ', caplog) def test__send_msg_with_json_format(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() default_conf["webhook"]["format"] = "json" webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = {'text': 'Hello'} post = MagicMock() mocker.patch("freqtrade.rpc.webhook.post", post) webhook._send_msg(msg) assert post.call_args[1] == {'json': msg} def test__send_msg_with_raw_format(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() default_conf["webhook"]["format"] = "raw" webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = {'data': 'Hello'} post = MagicMock() mocker.patch("freqtrade.rpc.webhook.post", post) webhook._send_msg(msg) 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]