Merge pull request #4766 from freqtrade/fill_rpc
Add buy/sell fill messages to telegram
This commit is contained in:
commit
b230558294
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -82,12 +82,19 @@ Example configuration showing the different settings:
|
|||||||
"buy": "silent",
|
"buy": "silent",
|
||||||
"sell": "on",
|
"sell": "on",
|
||||||
"buy_cancel": "silent",
|
"buy_cancel": "silent",
|
||||||
"sell_cancel": "on"
|
"sell_cancel": "on",
|
||||||
|
"buy_fill": "off",
|
||||||
|
"sell_fill": "off"
|
||||||
},
|
},
|
||||||
"balance_dust_level": 0.01
|
"balance_dust_level": 0.01
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange.
|
||||||
|
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange.
|
||||||
|
`*_fill` notifications are off by default and must be explicitly enabled.
|
||||||
|
|
||||||
|
|
||||||
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||||
|
|
||||||
## Create a custom keyboard (command shortcut buttons)
|
## Create a custom keyboard (command shortcut buttons)
|
||||||
|
@ -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`
|
||||||
|
@ -246,14 +246,24 @@ CONF_SCHEMA = {
|
|||||||
'balance_dust_level': {'type': 'number', 'minimum': 0.0},
|
'balance_dust_level': {'type': 'number', 'minimum': 0.0},
|
||||||
'notification_settings': {
|
'notification_settings': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
|
'default': {},
|
||||||
'properties': {
|
'properties': {
|
||||||
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
|
||||||
'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}
|
'buy_fill': {'type': 'string',
|
||||||
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
|
'default': 'off'
|
||||||
|
},
|
||||||
|
'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
|
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
|
'sell_fill': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
|
'default': 'off'
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -113,7 +113,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
via RPC about changes in the bot status.
|
via RPC about changes in the bot status.
|
||||||
"""
|
"""
|
||||||
self.rpc.send_msg({
|
self.rpc.send_msg({
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
'type': RPCMessageType.STATUS,
|
||||||
'status': msg
|
'status': msg
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
if len(open_trades) != 0:
|
if len(open_trades) != 0:
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
'type': RPCMessageType.WARNING,
|
||||||
'status': f"{len(open_trades)} open trades active.\n\n"
|
'status': f"{len(open_trades)} open trades active.\n\n"
|
||||||
f"Handle these trades manually on {self.exchange.name}, "
|
f"Handle these trades manually on {self.exchange.name}, "
|
||||||
f"or '/start' the bot again and use '/stopbuy' "
|
f"or '/start' the bot again and use '/stopbuy' "
|
||||||
@ -634,7 +634,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"""
|
"""
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
'type': RPCMessageType.BUY,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'limit': trade.open_rate,
|
'limit': trade.open_rate,
|
||||||
@ -658,7 +658,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.BUY_CANCEL,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'limit': trade.open_rate,
|
'limit': trade.open_rate,
|
||||||
@ -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,
|
||||||
|
'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 if fill
|
||||||
|
else RPCMessageType.SELL),
|
||||||
'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,
|
||||||
@ -1267,7 +1284,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
gain = "profit" if profit_ratio > 0 else "loss"
|
gain = "profit" if profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.SELL_CANCEL,
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
@ -1344,9 +1361,15 @@ 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:
|
||||||
|
if not stoploss_order and not trade.open_order_id:
|
||||||
|
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 not trade.open_order_id:
|
||||||
|
# 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,
|
||||||
|
@ -31,13 +31,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RPCMessageType(Enum):
|
class RPCMessageType(Enum):
|
||||||
STATUS_NOTIFICATION = 'status'
|
STATUS = 'status'
|
||||||
WARNING_NOTIFICATION = 'warning'
|
WARNING = 'warning'
|
||||||
STARTUP_NOTIFICATION = 'startup'
|
STARTUP = 'startup'
|
||||||
BUY_NOTIFICATION = 'buy'
|
BUY = 'buy'
|
||||||
BUY_CANCEL_NOTIFICATION = 'buy_cancel'
|
BUY_FILL = 'buy_fill'
|
||||||
SELL_NOTIFICATION = 'sell'
|
BUY_CANCEL = 'buy_cancel'
|
||||||
SELL_CANCEL_NOTIFICATION = 'sell_cancel'
|
SELL = 'sell'
|
||||||
|
SELL_FILL = 'sell_fill'
|
||||||
|
SELL_CANCEL = 'sell_cancel'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
@ -67,7 +67,7 @@ class RPCManager:
|
|||||||
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
|
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
'type': RPCMessageType.WARNING,
|
||||||
'status': 'Dry run is enabled. All trades are simulated.'
|
'status': 'Dry run is enabled. All trades are simulated.'
|
||||||
})
|
})
|
||||||
stake_currency = config['stake_currency']
|
stake_currency = config['stake_currency']
|
||||||
@ -79,7 +79,7 @@ class RPCManager:
|
|||||||
exchange_name = config['exchange']['name']
|
exchange_name = config['exchange']['name']
|
||||||
strategy_name = config.get('strategy', '')
|
strategy_name = config.get('strategy', '')
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
'type': RPCMessageType.STARTUP_NOTIFICATION,
|
'type': RPCMessageType.STARTUP,
|
||||||
'status': f'*Exchange:* `{exchange_name}`\n'
|
'status': f'*Exchange:* `{exchange_name}`\n'
|
||||||
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
||||||
f'*Minimum ROI:* `{minimal_roi}`\n'
|
f'*Minimum ROI:* `{minimal_roi}`\n'
|
||||||
@ -88,13 +88,13 @@ class RPCManager:
|
|||||||
f'*Strategy:* `{strategy_name}`'
|
f'*Strategy:* `{strategy_name}`'
|
||||||
})
|
})
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
'type': RPCMessageType.STARTUP_NOTIFICATION,
|
'type': RPCMessageType.STARTUP,
|
||||||
'status': f'Searching for {stake_currency} pairs to buy and sell '
|
'status': f'Searching for {stake_currency} pairs to buy and sell '
|
||||||
f'based on {pairlist.short_desc()}'
|
f'based on {pairlist.short_desc()}'
|
||||||
})
|
})
|
||||||
if len(protections.name_list) > 0:
|
if len(protections.name_list) > 0:
|
||||||
prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()])
|
prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()])
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
'type': RPCMessageType.STARTUP_NOTIFICATION,
|
'type': RPCMessageType.STARTUP,
|
||||||
'status': f'Using Protections: \n{prots}'
|
'status': f'Using Protections: \n{prots}'
|
||||||
})
|
})
|
||||||
|
@ -176,6 +176,53 @@ class Telegram(RPCHandler):
|
|||||||
"""
|
"""
|
||||||
self._updater.stop()
|
self._updater.stop()
|
||||||
|
|
||||||
|
def _format_buy_msg(self, msg: Dict[str, Any]) -> str:
|
||||||
|
if self._rpc._fiat_converter:
|
||||||
|
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||||
|
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
|
else:
|
||||||
|
msg['stake_amount_fiat'] = 0
|
||||||
|
|
||||||
|
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
|
||||||
|
f" (#{msg['trade_id']})\n"
|
||||||
|
f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||||
|
f"*Open Rate:* `{msg['limit']:.8f}`\n"
|
||||||
|
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||||
|
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
|
||||||
|
|
||||||
|
if msg.get('fiat_currency', None):
|
||||||
|
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||||
|
message += ")`"
|
||||||
|
return message
|
||||||
|
|
||||||
|
def _format_sell_msg(self, msg: Dict[str, Any]) -> str:
|
||||||
|
msg['amount'] = round(msg['amount'], 8)
|
||||||
|
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
|
||||||
|
msg['duration'] = msg['close_date'].replace(
|
||||||
|
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||||
|
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||||
|
|
||||||
|
msg['emoji'] = self._get_sell_emoji(msg)
|
||||||
|
|
||||||
|
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
|
||||||
|
"*Amount:* `{amount:.8f}`\n"
|
||||||
|
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||||
|
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||||
|
"*Close Rate:* `{limit:.8f}`\n"
|
||||||
|
"*Sell Reason:* `{sell_reason}`\n"
|
||||||
|
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
||||||
|
"*Profit:* `{profit_percent:.2f}%`").format(**msg)
|
||||||
|
|
||||||
|
# Check if all sell properties are available.
|
||||||
|
# This might not be the case if the message origin is triggered by /forcesell
|
||||||
|
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
||||||
|
and self._rpc._fiat_converter):
|
||||||
|
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||||
|
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
|
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
|
||||||
|
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
||||||
|
return message
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
""" Send a message to telegram channel """
|
""" Send a message to telegram channel """
|
||||||
|
|
||||||
@ -186,67 +233,31 @@ class Telegram(RPCHandler):
|
|||||||
# Notification disabled
|
# Notification disabled
|
||||||
return
|
return
|
||||||
|
|
||||||
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
|
if msg['type'] == RPCMessageType.BUY:
|
||||||
if self._rpc._fiat_converter:
|
message = self._format_buy_msg(msg)
|
||||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
|
||||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
|
||||||
else:
|
|
||||||
msg['stake_amount_fiat'] = 0
|
|
||||||
|
|
||||||
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
|
elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
||||||
f" (#{msg['trade_id']})\n"
|
msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell'
|
||||||
f"*Amount:* `{msg['amount']:.8f}`\n"
|
|
||||||
f"*Open Rate:* `{msg['limit']:.8f}`\n"
|
|
||||||
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
|
||||||
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
|
|
||||||
|
|
||||||
if msg.get('fiat_currency', None):
|
|
||||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
|
||||||
message += ")`"
|
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
|
||||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||||
"Cancelling open buy Order for {pair} (#{trade_id}). "
|
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
||||||
"Reason: {reason}.".format(**msg))
|
"Reason: {reason}.".format(**msg))
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL):
|
||||||
msg['amount'] = round(msg['amount'], 8)
|
msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell'
|
||||||
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
|
|
||||||
msg['duration'] = msg['close_date'].replace(
|
|
||||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
|
||||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
|
||||||
|
|
||||||
msg['emoji'] = self._get_sell_emoji(msg)
|
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||||
|
"Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg))
|
||||||
|
|
||||||
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
|
elif msg['type'] == RPCMessageType.SELL:
|
||||||
"*Amount:* `{amount:.8f}`\n"
|
message = self._format_sell_msg(msg)
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
|
||||||
"*Close Rate:* `{limit:.8f}`\n"
|
|
||||||
"*Sell Reason:* `{sell_reason}`\n"
|
|
||||||
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
|
||||||
"*Profit:* `{profit_percent:.2f}%`").format(**msg)
|
|
||||||
|
|
||||||
# Check if all sell properties are available.
|
elif msg['type'] == RPCMessageType.STATUS:
|
||||||
# This might not be the case if the message origin is triggered by /forcesell
|
|
||||||
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
|
||||||
and self._rpc._fiat_converter):
|
|
||||||
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
|
||||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
|
||||||
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
|
|
||||||
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
|
||||||
message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
|
|
||||||
"for {pair} (#{trade_id}). Reason: {reason}").format(**msg)
|
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
|
||||||
message = '*Status:* `{status}`'.format(**msg)
|
message = '*Status:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.WARNING:
|
||||||
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.STARTUP:
|
||||||
message = '{status}'.format(**msg)
|
message = '{status}'.format(**msg)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -45,17 +45,21 @@ class Webhook(RPCHandler):
|
|||||||
""" Send a message to telegram channel """
|
""" Send a message to telegram channel """
|
||||||
try:
|
try:
|
||||||
|
|
||||||
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
|
if msg['type'] == RPCMessageType.BUY:
|
||||||
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:
|
||||||
valuedict = self._config['webhook'].get('webhookbuycancel', None)
|
valuedict = self._config['webhook'].get('webhookbuycancel', None)
|
||||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.BUY_FILL:
|
||||||
|
valuedict = self._config['webhook'].get('webhookbuyfill', None)
|
||||||
|
elif msg['type'] == RPCMessageType.SELL:
|
||||||
valuedict = self._config['webhook'].get('webhooksell', None)
|
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.SELL_FILL:
|
||||||
|
valuedict = self._config['webhook'].get('webhooksellfill', None)
|
||||||
|
elif msg['type'] == RPCMessageType.SELL_CANCEL:
|
||||||
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,
|
||||||
RPCMessageType.STARTUP_NOTIFICATION,
|
RPCMessageType.STARTUP,
|
||||||
RPCMessageType.WARNING_NOTIFICATION):
|
RPCMessageType.WARNING):
|
||||||
valuedict = self._config['webhook'].get('webhookstatus', None)
|
valuedict = self._config['webhook'].get('webhookstatus', None)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||||
|
@ -314,7 +314,8 @@ def get_default_conf(testdatadir):
|
|||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"token": "token",
|
"token": "token",
|
||||||
"chat_id": "0"
|
"chat_id": "0",
|
||||||
|
"notification_settings": {},
|
||||||
},
|
},
|
||||||
"datadir": str(testdatadir),
|
"datadir": str(testdatadir),
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
|
@ -71,7 +71,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.send_msg({
|
rpc_manager.send_msg({
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
'type': RPCMessageType.STATUS,
|
||||||
'status': 'test'
|
'status': 'test'
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.send_msg({
|
rpc_manager.send_msg({
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
'type': RPCMessageType.STATUS,
|
||||||
'status': 'test'
|
'status': 'test'
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non
|
|||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION,
|
rpc_manager.send_msg({'type': RPCMessageType.STARTUP,
|
||||||
'status': 'TestMessage'})
|
'status': 'TestMessage'})
|
||||||
assert log_has(
|
assert log_has(
|
||||||
"Message type 'startup' not implemented by handler webhook.",
|
"Message type 'startup' not implemented by handler webhook.",
|
||||||
|
@ -683,10 +683,10 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
context.args = ["1"]
|
context.args = ["1"]
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forcesell(update=update, context=context)
|
||||||
|
|
||||||
assert msg_mock.call_count == 3
|
assert msg_mock.call_count == 4
|
||||||
last_msg = msg_mock.call_args_list[-1][0][0]
|
last_msg = msg_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -703,6 +703,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
|
'close_rate': ANY,
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
@ -743,11 +744,11 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
context.args = ["1"]
|
context.args = ["1"]
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forcesell(update=update, context=context)
|
||||||
|
|
||||||
assert msg_mock.call_count == 3
|
assert msg_mock.call_count == 4
|
||||||
|
|
||||||
last_msg = msg_mock.call_args_list[-1][0][0]
|
last_msg = msg_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -764,6 +765,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
|
'close_rate': ANY,
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
@ -794,11 +796,11 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
context.args = ["all"]
|
context.args = ["all"]
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forcesell(update=update, context=context)
|
||||||
|
|
||||||
# Called for each trade 3 times
|
# Called for each trade 4 times
|
||||||
assert msg_mock.call_count == 8
|
assert msg_mock.call_count == 12
|
||||||
msg = msg_mock.call_args_list[1][0][0]
|
msg = msg_mock.call_args_list[2][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -815,6 +817,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
|
'close_rate': ANY,
|
||||||
} == msg
|
} == msg
|
||||||
|
|
||||||
|
|
||||||
@ -1195,7 +1198,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
|
|||||||
def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
|
def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
|
||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
'type': RPCMessageType.BUY,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -1240,7 +1243,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
|
|||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.BUY_CANCEL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -1251,6 +1254,25 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
|
|||||||
'Reason: cancelled due to timeout.')
|
'Reason: cancelled due to timeout.')
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
|
||||||
|
|
||||||
|
default_conf['telegram']['notification_settings']['buy_fill'] = 'on'
|
||||||
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.BUY_FILL,
|
||||||
|
'trade_id': 1,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'pair': 'ETH/USDT',
|
||||||
|
'open_rate': 200,
|
||||||
|
'stake_amount': 100,
|
||||||
|
'amount': 0.5,
|
||||||
|
'open_date': arrow.utcnow().datetime
|
||||||
|
})
|
||||||
|
assert (msg_mock.call_args[0][0] == '\N{LARGE CIRCLE} *Binance:* '
|
||||||
|
'Buy order for ETH/USDT (#1) filled for 200.')
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
|
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
@ -1258,7 +1280,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
old_convamount = telegram._rpc._fiat_converter.convert_amount
|
old_convamount = telegram._rpc._fiat_converter.convert_amount
|
||||||
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'KEY/ETH',
|
'pair': 'KEY/ETH',
|
||||||
@ -1288,7 +1310,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'KEY/ETH',
|
'pair': 'KEY/ETH',
|
||||||
@ -1325,27 +1347,27 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
|||||||
old_convamount = telegram._rpc._fiat_converter.convert_amount
|
old_convamount = telegram._rpc._fiat_converter.convert_amount
|
||||||
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.SELL_CANCEL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'KEY/ETH',
|
'pair': 'KEY/ETH',
|
||||||
'reason': 'Cancelled on exchange'
|
'reason': 'Cancelled on exchange'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).'
|
== ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).'
|
||||||
' Reason: Cancelled on exchange')
|
' Reason: Cancelled on exchange.')
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.SELL_CANCEL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'KEY/ETH',
|
'pair': 'KEY/ETH',
|
||||||
'reason': 'timeout'
|
'reason': 'timeout'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).'
|
== ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).'
|
||||||
' Reason: timeout')
|
' Reason: timeout.')
|
||||||
# Reset singleton function to avoid random breaks
|
# Reset singleton function to avoid random breaks
|
||||||
telegram._rpc._fiat_converter.convert_amount = old_convamount
|
telegram._rpc._fiat_converter.convert_amount = old_convamount
|
||||||
|
|
||||||
@ -1354,7 +1376,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
'type': RPCMessageType.STATUS,
|
||||||
'status': 'running'
|
'status': 'running'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == '*Status:* `running`'
|
assert msg_mock.call_args[0][0] == '*Status:* `running`'
|
||||||
@ -1363,7 +1385,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
|
|||||||
def test_warning_notification(default_conf, mocker) -> None:
|
def test_warning_notification(default_conf, mocker) -> None:
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
'type': RPCMessageType.WARNING,
|
||||||
'status': 'message'
|
'status': 'message'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`'
|
assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`'
|
||||||
@ -1372,7 +1394,7 @@ def test_warning_notification(default_conf, mocker) -> None:
|
|||||||
def test_startup_notification(default_conf, mocker) -> None:
|
def test_startup_notification(default_conf, mocker) -> None:
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.STARTUP_NOTIFICATION,
|
'type': RPCMessageType.STARTUP,
|
||||||
'status': '*Custom:* `Hello World`'
|
'status': '*Custom:* `Hello World`'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
|
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
|
||||||
@ -1391,7 +1413,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
'type': RPCMessageType.BUY,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -1417,7 +1439,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'KEY/ETH',
|
'pair': 'KEY/ETH',
|
||||||
|
@ -25,6 +25,11 @@ def get_webhook_dict() -> dict:
|
|||||||
"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": "{stake_amount:8f} {stake_currency}"
|
||||||
|
},
|
||||||
"webhooksell": {
|
"webhooksell": {
|
||||||
"value1": "Selling {pair}",
|
"value1": "Selling {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
@ -35,6 +40,11 @@ def get_webhook_dict() -> dict:
|
|||||||
"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": "",
|
||||||
@ -49,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)
|
||||||
@ -58,7 +68,7 @@ def test_send_msg(default_conf, mocker):
|
|||||||
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)
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
'type': RPCMessageType.BUY,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 0.005,
|
'limit': 0.005,
|
||||||
@ -76,10 +86,10 @@ def test_send_msg(default_conf, mocker):
|
|||||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||||
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
|
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
|
||||||
# Test buy cancel
|
# Test buy cancel
|
||||||
msg_mock = MagicMock()
|
msg_mock.reset_mock()
|
||||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.BUY_CANCEL,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 0.005,
|
'limit': 0.005,
|
||||||
@ -96,11 +106,31 @@ 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 sell
|
# Test buy fill
|
||||||
msg_mock = MagicMock()
|
msg_mock.reset_mock()
|
||||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.BUY_FILL,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'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 = {
|
||||||
|
'type': RPCMessageType.SELL,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': "profit",
|
'gain': "profit",
|
||||||
@ -123,10 +153,9 @@ def test_send_msg(default_conf, mocker):
|
|||||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||||
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
|
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
|
||||||
# Test sell cancel
|
# Test sell cancel
|
||||||
msg_mock = MagicMock()
|
msg_mock.reset_mock()
|
||||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.SELL_CANCEL,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': "profit",
|
'gain': "profit",
|
||||||
@ -148,9 +177,35 @@ 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))
|
||||||
for msgtype in [RPCMessageType.STATUS_NOTIFICATION,
|
# Test Sell fill
|
||||||
RPCMessageType.WARNING_NOTIFICATION,
|
msg_mock.reset_mock()
|
||||||
RPCMessageType.STARTUP_NOTIFICATION]:
|
msg = {
|
||||||
|
'type': RPCMessageType.SELL_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': 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,
|
||||||
|
RPCMessageType.WARNING,
|
||||||
|
RPCMessageType.STARTUP]:
|
||||||
# Test notification
|
# Test notification
|
||||||
msg = {
|
msg = {
|
||||||
'type': msgtype,
|
'type': msgtype,
|
||||||
@ -173,8 +228,8 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
|||||||
del default_conf["webhook"]["webhookbuy"]
|
del default_conf["webhook"]["webhookbuy"]
|
||||||
|
|
||||||
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||||
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
|
webhook.send_msg({'type': RPCMessageType.BUY})
|
||||||
assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks",
|
assert log_has(f"Message type '{RPCMessageType.BUY}' not configured for webhooks",
|
||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
default_conf["webhook"] = get_webhook_dict()
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
@ -183,7 +238,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
|||||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
'type': RPCMessageType.BUY,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 0.005,
|
'limit': 0.005,
|
||||||
|
@ -1710,6 +1710,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
|
|||||||
open_rate=0.01,
|
open_rate=0.01,
|
||||||
open_date=arrow.utcnow().datetime,
|
open_date=arrow.utcnow().datetime,
|
||||||
amount=11,
|
amount=11,
|
||||||
|
exchange="binance",
|
||||||
)
|
)
|
||||||
assert not freqtrade.update_trade_state(trade, None)
|
assert not freqtrade.update_trade_state(trade, None)
|
||||||
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
|
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
|
||||||
@ -2319,7 +2320,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
|
|||||||
# note this is for a partially-complete buy order
|
# note this is for a partially-complete buy order
|
||||||
freqtrade.check_handle_timedout()
|
freqtrade.check_handle_timedout()
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 2
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||||
assert len(trades) == 1
|
assert len(trades) == 1
|
||||||
assert trades[0].amount == 23.0
|
assert trades[0].amount == 23.0
|
||||||
@ -2354,7 +2355,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
|
|||||||
assert log_has_re(r"Applying fee on amount for Trade.*", caplog)
|
assert log_has_re(r"Applying fee on amount for Trade.*", caplog)
|
||||||
|
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 2
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||||
assert len(trades) == 1
|
assert len(trades) == 1
|
||||||
# Verify that trade has been updated
|
# Verify that trade has been updated
|
||||||
@ -2394,7 +2395,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade,
|
|||||||
assert log_has_re(r"Could not update trade amount: .*", caplog)
|
assert log_has_re(r"Could not update trade amount: .*", caplog)
|
||||||
|
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 2
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||||
assert len(trades) == 1
|
assert len(trades) == 1
|
||||||
# Verify that trade has been updated
|
# Verify that trade has been updated
|
||||||
@ -2623,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
|
|||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': 'profit',
|
'gain': 'profit',
|
||||||
@ -2639,6 +2640,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
|
|||||||
'sell_reason': SellType.ROI.value,
|
'sell_reason': SellType.ROI.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
|
'close_rate': ANY,
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
@ -2672,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
|
|||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -2689,6 +2691,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
|
|||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
|
'close_rate': ANY,
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
@ -2729,7 +2732,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
|||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -2746,7 +2749,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
|||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
|
'close_rate': ANY,
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
@ -2830,7 +2833,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
|||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
assert cancel_order.call_count == 1
|
assert cancel_order.call_count == 1
|
||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee,
|
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee,
|
||||||
@ -2898,7 +2901,10 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
|||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
assert trade.is_open is False
|
assert trade.is_open is False
|
||||||
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 3
|
||||||
|
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY
|
||||||
|
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL
|
||||||
|
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_market_order(default_conf, ticker, fee,
|
def test_execute_sell_market_order(default_conf, ticker, fee,
|
||||||
@ -2932,10 +2938,10 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
|
|||||||
assert not trade.is_open
|
assert not trade.is_open
|
||||||
assert trade.close_profit == 0.0620716
|
assert trade.close_profit == 0.0620716
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 3
|
||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -2952,6 +2958,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
|
|||||||
'sell_reason': SellType.ROI.value,
|
'sell_reason': SellType.ROI.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
|
'close_rate': ANY,
|
||||||
|
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user