Add buy and sell fill notifications

closes #3542
This commit is contained in:
Matthias 2021-04-19 19:58:29 +02:00
parent 71b017e7c3
commit fecd5c582b
7 changed files with 136 additions and 5 deletions

View File

@ -163,7 +163,9 @@
"warning": "on", "warning": "on",
"startup": "on", "startup": "on",
"buy": "on", "buy": "on",
"buy_fill": "on",
"sell": "on", "sell": "on",
"sell_fill": "on",
"buy_cancel": "on", "buy_cancel": "on",
"sell_cancel": "on" "sell_cancel": "on"
} }

View File

@ -19,6 +19,11 @@ Sample configuration (tested using IFTTT).
"value1": "Cancelling Open Buy Order for {pair}", "value1": "Cancelling Open Buy Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}" "value3": "{stake_amount:8f} {stake_currency}"
},
"webhookbuyfill": {
"value1": "Buy Order for {pair} filled",
"value2": "at {open_rate:8f}",
"value3": ""
}, },
"webhooksell": { "webhooksell": {
"value1": "Selling {pair}", "value1": "Selling {pair}",
@ -30,6 +35,11 @@ Sample configuration (tested using IFTTT).
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellfill": {
"value1": "Sell Order for {pair} filled",
"value2": "at {close_rate:8f}.",
"value3": ""
},
"webhookstatus": { "webhookstatus": {
"value1": "Status: {status}", "value1": "Status: {status}",
"value2": "", "value2": "",
@ -91,6 +101,21 @@ Possible parameters are:
* `order_type` * `order_type`
* `current_rate` * `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 ### Webhooksell
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. 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` * `limit`
* `amount` * `amount`
* `open_rate` * `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` * `current_rate`
* `profit_amount` * `profit_amount`
* `profit_ratio` * `profit_ratio`

View File

@ -675,6 +675,21 @@ class FreqtradeBot(LoggingMixin):
# Send the message # Send the message
self.rpc.send_msg(msg) 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 # SELL / exit positions / close trades logic and methods
# #
@ -1212,19 +1227,20 @@ class FreqtradeBot(LoggingMixin):
return True 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. Sends rpc notification when a sell occured.
""" """
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached rates here - it was updated seconds ago. # 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) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
msg = { msg = {
'type': RPCMessageType.SELL_NOTIFICATION, 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill
else RPCMessageType.SELL_NOTIFICATION),
'trade_id': trade.id, 'trade_id': trade.id,
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -1233,6 +1249,7 @@ class FreqtradeBot(LoggingMixin):
'order_type': order_type, 'order_type': order_type,
'amount': trade.amount, 'amount': trade.amount,
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
'close_rate': trade.close_rate,
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_trade, 'profit_amount': profit_trade,
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
@ -1344,9 +1361,14 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets when order is closed # Updating wallets when order is closed
if not trade.is_open: if not trade.is_open:
self._notify_sell(trade, '', True)
self.protections.stop_per_pair(trade.pair) self.protections.stop_per_pair(trade.pair)
self.protections.global_stop() self.protections.global_stop()
self.wallets.update() self.wallets.update()
elif trade.open_order_id is None:
# Buy fill
self._notify_buy_fill(trade)
return False return False
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,

View File

@ -35,8 +35,10 @@ class RPCMessageType(Enum):
WARNING_NOTIFICATION = 'warning' WARNING_NOTIFICATION = 'warning'
STARTUP_NOTIFICATION = 'startup' STARTUP_NOTIFICATION = 'startup'
BUY_NOTIFICATION = 'buy' BUY_NOTIFICATION = 'buy'
BUY_FILL_NOTIFICATION = 'buy_fill'
BUY_CANCEL_NOTIFICATION = 'buy_cancel' BUY_CANCEL_NOTIFICATION = 'buy_cancel'
SELL_NOTIFICATION = 'sell' SELL_NOTIFICATION = 'sell'
SELL_FILL_NOTIFICATION = 'sell_fill'
SELL_CANCEL_NOTIFICATION = 'sell_cancel' SELL_CANCEL_NOTIFICATION = 'sell_cancel'
def __repr__(self): def __repr__(self):

View File

@ -209,6 +209,10 @@ class Telegram(RPCHandler):
"Cancelling open buy Order for {pair} (#{trade_id}). " "Cancelling open buy Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg)) "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: elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
msg['amount'] = round(msg['amount'], 8) msg['amount'] = round(msg['amount'], 8)
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) 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 " message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
"for {pair} (#{trade_id}). Reason: {reason}").format(**msg) "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: elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg) message = '*Status:* `{status}`'.format(**msg)

View File

@ -49,8 +49,12 @@ class Webhook(RPCHandler):
valuedict = self._config['webhook'].get('webhookbuy', None) valuedict = self._config['webhook'].get('webhookbuy', None)
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhookbuycancel', None) 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: elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksell', None) 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: 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,

View File

@ -43,7 +43,7 @@ def get_webhook_dict() -> dict:
"webhooksellfill": { "webhooksellfill": {
"value1": "Sell Order for {pair} filled", "value1": "Sell Order for {pair} filled",
"value2": "at {close_rate:8f}", "value2": "at {close_rate:8f}",
"value3": "{stake_amount:8f} {stake_currency}" "value3": ""
}, },
"webhookstatus": { "webhookstatus": {
"value1": "Status: {status}", "value1": "Status: {status}",
@ -59,7 +59,7 @@ def test__init__(mocker, default_conf):
assert webhook._config == 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() default_conf["webhook"] = get_webhook_dict()
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) 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)) default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) 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 # Test sell
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
@ -156,6 +177,32 @@ def test_send_msg(default_conf, mocker):
default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg)) default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) 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, for msgtype in [RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION,
RPCMessageType.STARTUP_NOTIFICATION]: RPCMessageType.STARTUP_NOTIFICATION]: