parent
71b017e7c3
commit
fecd5c582b
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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`
|
||||||
|
@ -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,
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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]:
|
||||||
|
Loading…
Reference in New Issue
Block a user