From fecd5c582b81e191b82a5d90834b0592eb1ffbe7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:58:29 +0200 Subject: [PATCH] Add buy and sell fill notifications closes #3542 --- config_full.json.example | 2 ++ docs/webhook-config.md | 46 +++++++++++++++++++++++++++++++ freqtrade/freqtradebot.py | 28 ++++++++++++++++--- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 8 ++++++ freqtrade/rpc/webhook.py | 4 +++ tests/rpc/test_rpc_webhook.py | 51 +++++++++++++++++++++++++++++++++-- 7 files changed, 136 insertions(+), 5 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 717797933..973afe2c8 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -163,7 +163,9 @@ "warning": "on", "startup": "on", "buy": "on", + "buy_fill": "on", "sell": "on", + "sell_fill": "on", "buy_cancel": "on", "sell_cancel": "on" } diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 2e41ad2cc..8ce6edc18 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -19,6 +19,11 @@ Sample configuration (tested using IFTTT). "value1": "Cancelling Open Buy Order for {pair}", "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "" }, "webhooksell": { "value1": "Selling {pair}", @@ -30,6 +35,11 @@ Sample configuration (tested using IFTTT). "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}.", + "value3": "" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -91,6 +101,21 @@ Possible parameters are: * `order_type` * `current_rate` +### Webhookbuyfill + +The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `open_rate` +* `amount` +* `open_date` +* `stake_amount` +* `stake_currency` +* `fiat_currency` + ### Webhooksell The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. @@ -103,6 +128,27 @@ Possible parameters are: * `limit` * `amount` * `open_rate` +* `profit_amount` +* `profit_ratio` +* `stake_currency` +* `fiat_currency` +* `sell_reason` +* `order_type` +* `open_date` +* `close_date` + +### Webhooksellfill + +The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `gain` +* `close_rate` +* `amount` +* `open_rate` * `current_rate` * `profit_amount` * `profit_ratio` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..68f98ec21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -675,6 +675,21 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) + def _notify_buy_fill(self, trade: Trade) -> None: + msg = { + 'trade_id': trade.id, + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'open_rate': trade.open_rate, + 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': trade.amount, + 'open_date': trade.open_date, + } + self.rpc.send_msg(msg) + # # SELL / exit positions / close trades logic and methods # @@ -1212,19 +1227,20 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_sell(self, trade: Trade, order_type: str) -> None: + def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: """ Sends rpc notification when a sell occured. """ profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. - current_rate = self.get_sell_rate(trade.pair, False) + current_rate = self.get_sell_rate(trade.pair, False) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill + else RPCMessageType.SELL_NOTIFICATION), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1233,6 +1249,7 @@ class FreqtradeBot(LoggingMixin): 'order_type': order_type, 'amount': trade.amount, 'open_rate': trade.open_rate, + 'close_rate': trade.close_rate, 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, @@ -1344,9 +1361,14 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() + elif trade.open_order_id is None: + # Buy fill + self._notify_buy_fill(trade) + return False def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..bf0b88f6c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -35,8 +35,10 @@ class RPCMessageType(Enum): WARNING_NOTIFICATION = 'warning' STARTUP_NOTIFICATION = 'startup' BUY_NOTIFICATION = 'buy' + BUY_FILL_NOTIFICATION = 'buy_fill' BUY_CANCEL_NOTIFICATION = 'buy_cancel' SELL_NOTIFICATION = 'sell' + SELL_FILL_NOTIFICATION = 'sell_fill' SELL_CANCEL_NOTIFICATION = 'sell_cancel' def __repr__(self): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 09b7b235c..4dceeb46c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -209,6 +209,10 @@ class Telegram(RPCHandler): "Cancelling open buy Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) @@ -240,6 +244,10 @@ class Telegram(RPCHandler): message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 5a30a9be8..c7e012af5 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -49,8 +49,12 @@ class Webhook(RPCHandler): valuedict = self._config['webhook'].get('webhookbuy', None) elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhookbuycancel', None) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhookbuyfill', None) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksell', None) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhooksellfill', None) elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksellcancel', None) elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index bfb9cbb01..38d2fe539 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -43,7 +43,7 @@ def get_webhook_dict() -> dict: "webhooksellfill": { "value1": "Sell Order for {pair} filled", "value2": "at {close_rate:8f}", - "value3": "{stake_amount:8f} {stake_currency}" + "value3": "" }, "webhookstatus": { "value1": "Status: {status}", @@ -59,7 +59,7 @@ def test__init__(mocker, default_conf): assert webhook._config == default_conf -def test_send_msg(default_conf, mocker): +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) @@ -106,6 +106,27 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) + # Test buy fill + msg_mock.reset_mock() + + msg = { + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + '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"]["webhookbuyfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) # Test sell msg_mock.reset_mock() msg = { @@ -156,6 +177,32 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) + # Test Sell fill + msg_mock.reset_mock() + msg = { + 'type': RPCMessageType.SELL_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + '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': SellType.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"]["webhooksellfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhooksellfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) + for msgtype in [RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION, RPCMessageType.STARTUP_NOTIFICATION]: