From f3b1161640507d91e7694f03e80095304bfdaf0a Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Sat, 8 Feb 2020 21:02:52 +0100 Subject: [PATCH] wide notifications fixes --- docs/configuration.md | 2 + docs/telegram-usage.md | 2 +- docs/webhook-config.md | 51 ++++++++++++ freqtrade/constants.py | 4 +- freqtrade/freqtradebot.py | 137 ++++++++++++++++++++++++--------- freqtrade/rpc/rpc.py | 14 ++-- freqtrade/rpc/telegram.py | 20 +++-- freqtrade/rpc/webhook.py | 4 + tests/rpc/test_rpc.py | 6 +- tests/rpc/test_rpc_telegram.py | 81 ++++++++++++++++--- tests/rpc/test_rpc_webhook.py | 46 ++++++----- tests/test_freqtradebot.py | 6 +- 12 files changed, 288 insertions(+), 85 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c0404d647..53e554709 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -92,7 +92,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `webhook.enabled` | Enable usage of Webhook notifications
***Datatype:*** *Boolean* | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* | `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhookbuycancel` | Payload to send on buy order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* | `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhooksellcancel` | Payload to send on sell order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* | `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* | `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Boolean* | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *IPv4* diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index ed0c21a6e..ac9cea3d6 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -55,7 +55,7 @@ official commands. You can ask at any moment for help with `/help`. | `/reload_conf` | | Reloads the configuration file | `/show_config` | | Shows part of the current configuration with relevant settings to operation | `/status` | | Lists all open trades -| `/status table` | | List all open trades in a table format +| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk(*) | `/count` | | Displays number of trades used and available | `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | | Instantly sells the given trade (Ignoring `minimum_roi`). diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 9e0a34eae..878b18e8a 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -15,11 +15,21 @@ Sample configuration (tested using IFTTT). "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" }, + "webhookbuycancel": { + "value1": "Cancelling Buy {pair}", + "value2": "limit {limit:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhooksell": { "value1": "Selling {pair}", "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency}" }, + "webhooksellcancel": { + "value1": "Cancelling Sell {pair}", + "value2": "limit {limit:8f}", + "value3": "profit: {profit_amount:8f} {stake_currency}" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -40,10 +50,29 @@ Possible parameters are: * `exchange` * `pair` * `limit` +* `amount` * `stake_amount` * `stake_currency` * `fiat_currency` * `order_type` +* `open_rate` +* `current_rate` + +### Webhookbuycancel + +The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format. +Possible parameters are: + +* `exchange` +* `pair` +* `limit` +* `amount` +* `stake_amount` +* `stake_currency` +* `fiat_currency` +* `order_type` +* `open_rate` +* `current_rate` ### Webhooksell @@ -57,6 +86,7 @@ Possible parameters are: * `amount` * `open_rate` * `current_rate` +* `close_rate` * `profit_amount` * `profit_percent` * `stake_currency` @@ -66,6 +96,27 @@ Possible parameters are: * `open_date` * `close_date` +### Webhooksellcancel + +The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format. +Possible parameters are: + +* `exchange` +* `pair` +* `gain` +* `limit` +* `amount` +* `open_rate` +* `current_rate` +* `close_rate` +* `profit_amount` +* `profit_percent` +* `stake_currency` +* `fiat_currency` +* `sell_reason` +* `order_type` +* `open_date` + ### Webhookstatus The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e68e741af..b34805e94 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -78,7 +78,7 @@ CONF_SCHEMA = { 'amend_last_stake_amount': {'type': 'boolean', 'default': False}, 'last_stake_amount_min_ratio': { 'type': 'number', 'minimum': 0.0, 'maximum': 1.0, 'default': 0.5 - }, + }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, @@ -191,7 +191,9 @@ CONF_SCHEMA = { 'properties': { 'enabled': {'type': 'boolean'}, 'webhookbuy': {'type': 'object'}, + 'webhookbuycancel': {'type': 'object'}, 'webhooksell': {'type': 'object'}, + 'webhooksellcancel': {'type': 'object'}, 'webhookstatus': {'type': 'object'}, }, }, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e51b3d550..2f57ca41b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -234,7 +234,7 @@ class FreqtradeBot: return trades_created - def get_buy_rate(self, pair: str, tick: Dict = None) -> float: + def get_buy_rate(self, pair: str, refresh: bool = False, tick: Dict = None) -> float: """ Calculates bid target between current ask price and last price :return: float: Price @@ -253,7 +253,7 @@ class FreqtradeBot: else: if not tick: logger.info('Using Last Ask / Last Price') - ticker = self.exchange.fetch_ticker(pair) + ticker = self.exchange.fetch_ticker(pair, refresh) else: ticker = tick if ticker['ask'] < ticker['last']: @@ -404,7 +404,7 @@ class FreqtradeBot: stake_amount = self.get_trade_stake_amount(pair) if not stake_amount: - logger.debug("Stake amount is 0, ignoring possible trade for {pair}.") + logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False logger.info(f"Buy signal found: about create a new trade with stake_amount: " @@ -414,10 +414,12 @@ class FreqtradeBot: if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): if self._check_depth_of_market_buy(pair, bid_check_dom): + logger.info(f'Executed Buy for {pair}.') return self.execute_buy(pair, stake_amount) else: return False + logger.info(f'Executed Buy for {pair}') return self.execute_buy(pair, stake_amount) else: return False @@ -450,7 +452,7 @@ class FreqtradeBot: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY - :return: None + :return: bool """ time_in_force = self.strategy.order_time_in_force['buy'] @@ -458,7 +460,7 @@ class FreqtradeBot: buy_limit_requested = price else: # Calculate price - buy_limit_requested = self.get_buy_rate(pair) + buy_limit_requested = self.get_buy_rate(pair, True) min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested) if min_stake_amount is not None and min_stake_amount > stake_amount: @@ -547,11 +549,37 @@ class FreqtradeBot: 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, - 'limit': trade.open_rate, + 'limit': trade.open_rate_requested, 'order_type': order_type, '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 or datetime.utcnow(), + 'current_rate': trade.open_rate_requested, + } + + # Send the message + self.rpc.send_msg(msg) + + def _notify_buy_cancel(self, trade: Trade, order_type: str) -> None: + """ + Sends rpc notification when a buy cancel occured. + """ + current_rate = self.get_buy_rate(trade.pair, False) + + msg = { + 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'limit': trade.open_rate_requested, + 'order_type': order_type, + '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, + 'current_rate': current_rate, } # Send the message @@ -587,7 +615,7 @@ class FreqtradeBot: return trades_closed - def get_sell_rate(self, pair: str, refresh: bool) -> float: + def get_sell_rate(self, pair: str, refresh: bool = False) -> float: """ Get sell rate - either using get-ticker bid or first bid based on orderbook The orderbook portion is only used for rpc messaging, which would otherwise fail @@ -751,7 +779,7 @@ class FreqtradeBot: update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: # cancelling the current stoploss on exchange first - logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})' + logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) ' 'in order to add another one ...', order['id']) try: self.exchange.cancel_order(order['id'], trade.pair) @@ -777,7 +805,7 @@ class FreqtradeBot: if should_sell.sell_flag: self.execute_sell(trade, sell_rate, should_sell.sell_type) - logger.info('executed sell, reason: %s', should_sell.sell_type) + logger.info(f'Executed Sell for {trade.pair}. Reason: {should_sell.sell_type}') return True return False @@ -820,41 +848,41 @@ class FreqtradeBot: if ((order['side'] == 'buy' and order['status'] == 'canceled') or (self._check_timed_out('buy', order))): - self.handle_timedout_limit_buy(trade, order) self.wallets.update() + order_type = self.strategy.order_types['buy'] + self._notify_buy_cancel(trade, order_type) elif ((order['side'] == 'sell' and order['status'] == 'canceled') or (self._check_timed_out('sell', order))): self.handle_timedout_limit_sell(trade, order) self.wallets.update() + order_type = self.strategy.order_types['sell'] + self._notify_sell_cancel(trade, order_type) - def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None: - """Close trade in database and send message""" + def delete_trade(self, trade: Trade) -> None: + """Delete trade in database""" Trade.session.delete(trade) Trade.session.flush() - logger.info('Buy order %s for %s.', reason, trade) - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Unfilled buy order for {trade.pair} {reason}' - }) def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool: """ Buy timeout - cancel order :return: True if order was fully cancelled """ - reason = "cancelled due to timeout" if order['status'] != 'canceled': + reason = "cancelled due to timeout" corder = self.exchange.cancel_order(trade.open_order_id, trade.pair) + logger.info('Buy order %s for %s.', reason, trade) else: # Order was cancelled already, so we can reuse the existing dict corder = order - reason = "canceled on Exchange" + reason = "cancelled on exchange" + logger.info('Buy order %s for %s.', reason, trade) if corder.get('remaining', order['remaining']) == order['amount']: # if trade is not partially completed, just delete the trade - self.handle_buy_order_full_cancel(trade, reason) + self.delete_trade(trade) return True # if trade is partially complete, edit the stake details for the trade @@ -878,10 +906,6 @@ class FreqtradeBot: trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Remaining buy order for {trade.pair} cancelled due to timeout' - }) return False def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool: @@ -889,24 +913,22 @@ class FreqtradeBot: Sell timeout - cancel order and update trade :return: True if order was fully cancelled """ + # if trade is not partially completed, just cancel the trade if order['remaining'] == order['amount']: - # if trade is not partially completed, just cancel the trade if order["status"] != "canceled": - reason = "due to timeout" + reason = "cancelled due to timeout" + # if trade is not partially completed, just delete the trade self.exchange.cancel_order(trade.open_order_id, trade.pair) - logger.info('Sell order timeout for %s.', trade) + logger.info('Sell order %s for %s.', reason, trade) else: - reason = "on exchange" - logger.info('Sell order canceled on exchange for %s.', trade) + reason = "cancelled on exchange" + logger.info('Sell order %s for %s.', reason, trade) + trade.close_rate = None trade.close_profit = None trade.close_date = None trade.is_open = True trade.open_order_id = None - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Unfilled sell order for {trade.pair} cancelled {reason}' - }) return True @@ -938,13 +960,13 @@ class FreqtradeBot: raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order :param sellreason: Reason the sell was triggered - :return: None + :return: bool """ sell_type = 'sell' if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): @@ -965,7 +987,7 @@ class FreqtradeBot: order_type = self.strategy.order_types[sell_type] if sell_reason == SellType.EMERGENCY_SELL: - # Emergencysells (default to market!) + # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") amount = self._safe_sell_amount(trade.pair, trade.amount) @@ -990,6 +1012,8 @@ class FreqtradeBot: self._notify_sell(trade, order_type) + return True + def _notify_sell(self, trade: Trade, order_type: str) -> None: """ Sends rpc notification when a sell occured. @@ -1006,7 +1030,7 @@ class FreqtradeBot: 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, - 'limit': trade.close_rate_requested, + 'limit': profit_rate, 'order_type': order_type, 'amount': trade.amount, 'open_rate': trade.open_rate, @@ -1017,6 +1041,45 @@ class FreqtradeBot: 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + } + + if 'fiat_display_currency' in self.config: + msg.update({ + 'fiat_currency': self.config['fiat_display_currency'], + }) + + # Send the message + self.rpc.send_msg(msg) + + def _notify_sell_cancel(self, trade: Trade, order_type: str) -> None: + """ + Sends rpc notification when a sell cancel 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 ticker here - it was updated seconds ago. + current_rate = self.get_sell_rate(trade.pair, False) + profit_percent = trade.calc_profit_ratio(profit_rate) + gain = "profit" if profit_percent > 0 else "loss" + + msg = { + 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'exchange': trade.exchange.capitalize(), + 'pair': trade.pair, + 'gain': gain, + 'limit': profit_rate, + 'order_type': order_type, + 'amount': trade.amount, + 'open_rate': trade.open_rate, + 'current_rate': current_rate, + 'profit_amount': profit_trade, + 'profit_percent': profit_percent, + 'sell_reason': trade.sell_reason, + 'open_date': trade.open_date, + 'close_date': trade.close_date, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), } if 'fiat_display_currency' in self.config: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7f5cfc101..c1efea79e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -26,7 +26,9 @@ class RPCMessageType(Enum): WARNING_NOTIFICATION = 'warning' CUSTOM_NOTIFICATION = 'custom' BUY_NOTIFICATION = 'buy' + BUY_CANCEL_NOTIFICATION = 'buy_cancel' SELL_NOTIFICATION = 'sell' + SELL_CANCEL_NOTIFICATION = 'sell_cancel' def __repr__(self): return self.value @@ -39,6 +41,7 @@ class RPCException(Exception): raise RPCException('*Status:* `no active trade`') """ + def __init__(self, message: str) -> None: super().__init__(self) self.message = message @@ -157,15 +160,16 @@ class RPC: profit_str = f'{trade_perc:.2f}%' if self._fiat_converter: fiat_profit = self._fiat_converter.convert_amount( - trade_profit, - stake_currency, - fiat_display_currency - ) + trade_profit, + stake_currency, + fiat_display_currency + ) if fiat_profit and not isnan(fiat_profit): profit_str += f" ({fiat_profit:.2f})" trades_list.append([ trade.id, - trade.pair, + trade.pair + ['', '*'][trade.open_order_id is not None + and trade.close_rate_requested is None], shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), profit_str ]) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e9ecdcff6..0dd7a8ffd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -134,13 +134,18 @@ class Telegram(RPC): msg['stake_amount_fiat'] = 0 message = ("*{exchange}:* Buying {pair}\n" - "at rate `{limit:.8f}\n" - "({stake_amount:.6f} {stake_currency}").format(**msg) + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{limit:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg) if msg.get('fiat_currency', None): - message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg) + message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg) message += ")`" + elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + message = "*{exchange}:* Cancelling Buy {pair}".format(**msg) + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) @@ -149,10 +154,10 @@ class Telegram(RPC): msg['duration_min'] = msg['duration'].total_seconds() / 60 message = ("*{exchange}:* Selling {pair}\n" - "*Rate:* `{limit:.8f}`\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) @@ -163,8 +168,11 @@ class Telegram(RPC): and self._fiat_converter): msg['profit_fiat'] = self._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) + message += (' `({gain}: {profit_amount:.8f} {stake_currency}' + ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) + + elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: + message = "*{exchange}:* Cancelling Sell {pair}".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 37ca466de..1309663d4 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -41,8 +41,12 @@ class Webhook(RPC): if msg['type'] == RPCMessageType.BUY_NOTIFICATION: 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.SELL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksell', None) + elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhooksellcancel', None) elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.CUSTOM_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION): diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 36fce1797..a35bfa0d6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -122,7 +122,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert "Since" in headers assert "Pair" in headers assert 'instantly' == result[0][2] - assert 'ETH/BTC' == result[0][1] + assert 'ETH/BTC' in result[0][1] assert '-0.59%' == result[0][3] # Test with fiatconvert @@ -131,7 +131,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert "Since" in headers assert "Pair" in headers assert 'instantly' == result[0][2] - assert 'ETH/BTC' == result[0][1] + assert 'ETH/BTC' in result[0][1] assert '-0.59% (-0.09)' == result[0][3] mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', @@ -140,7 +140,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: rpc._freqtrade.exchange._cached_ticker = {} result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] - assert 'ETH/BTC' == result[0][1] + assert 'ETH/BTC' in result[0][1] assert 'nan%' == result[0][3] diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ffc29ee12..ae9c0c4dc 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -284,7 +284,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ') assert int(fields[0]) == 1 - assert fields[1] == 'ETH/BTC' + assert 'ETH/BTC' in fields[1] assert msg_mock.call_count == 1 @@ -1200,12 +1200,35 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'stake_amount': 0.001, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', - 'fiat_currency': 'USD' + 'fiat_currency': 'USD', + 'current_rate': 1.099e-05, + 'amount': 1333.3333333333335, + 'open_date': arrow.utcnow().shift(hours=-1) }) assert msg_mock.call_args[0][0] \ == '*Bittrex:* Buying ETH/BTC\n' \ - 'at rate `0.00001099\n' \ - '(0.001000 BTC,0.000 USD)`' + '*Amount:* `1333.33333333`\n' \ + '*Open Rate:* `0.00001099`\n' \ + '*Current Rate:* `0.00001099`\n' \ + '*Total:* `(0.001000 BTC, 0.000 USD)`' + + +def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg({ + 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + }) + assert msg_mock.call_args[0][0] \ + == ('*Bittrex:* Cancelling Buy ETH/BTC') def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1239,13 +1262,13 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == ('*Binance:* Selling KEY/ETH\n' - '*Rate:* `0.00003201`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' + '*Close Rate:* `0.00003201`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1:00:00 (60.0 min)`\n' - '*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`') + '*Profit:* `-57.41%` `(loss: -0.05746268 ETH / -24.812 USD)`') msg_mock.reset_mock() telegram.send_msg({ @@ -1267,10 +1290,10 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == ('*Binance:* Selling KEY/ETH\n' - '*Rate:* `0.00003201`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' + '*Close Rate:* `0.00003201`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Profit:* `-57.41%`') @@ -1278,6 +1301,37 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram._fiat_converter.convert_amount = old_convamount +def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + old_convamount = telegram._fiat_converter.convert_amount + telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 + telegram.send_msg({ + 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + }) + assert msg_mock.call_args[0][0] \ + == ('*Binance:* Cancelling Sell KEY/ETH') + + msg_mock.reset_mock() + telegram.send_msg({ + 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + }) + assert msg_mock.call_args[0][0] \ + == ('*Binance:* Cancelling Sell KEY/ETH') + # Reset singleton function to avoid random breaks + telegram._fiat_converter.convert_amount = old_convamount + + def test_send_msg_status_notification(default_conf, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( @@ -1360,12 +1414,17 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'stake_amount': 0.001, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', - 'fiat_currency': None + 'fiat_currency': None, + 'current_rate': 1.099e-05, + 'amount': 1333.3333333333335, + 'open_date': arrow.utcnow().shift(hours=-1) }) assert msg_mock.call_args[0][0] \ == '*Bittrex:* Buying ETH/BTC\n' \ - 'at rate `0.00001099\n' \ - '(0.001000 BTC)`' + '*Amount:* `1333.33333333`\n' \ + '*Open Rate:* `0.00001099`\n' \ + '*Current Rate:* `0.00001099`\n' \ + '*Total:* `(0.001000 BTC)`' def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: @@ -1398,10 +1457,10 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == '*Binance:* Selling KEY/ETH\n' \ - '*Rate:* `0.00003201`\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00007500`\n' \ '*Current Rate:* `0.00003201`\n' \ + '*Close Rate:* `0.00003201`\n' \ '*Sell Reason:* `stop_loss`\n' \ '*Duration:* `2:35:03 (155.1 min)`\n' \ '*Profit:* `-57.41%`' diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index c066aa8e7..3b9ce3f0d 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -13,24 +13,34 @@ from tests.conftest import get_patched_freqtradebot, log_has def get_webhook_dict() -> dict: return { - "enabled": True, - "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/", - "webhookbuy": { - "value1": "Buying {pair}", - "value2": "limit {limit:8f}", - "value3": "{stake_amount:8f} {stake_currency}" - }, - "webhooksell": { - "value1": "Selling {pair}", - "value2": "limit {limit:8f}", - "value3": "profit: {profit_amount:8f} {stake_currency}" - }, - "webhookstatus": { - "value1": "Status: {status}", - "value2": "", - "value3": "" - } - } + "enabled": True, + "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/", + "webhookbuy": { + "value1": "Buying {pair}", + "value2": "limit {limit:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhookbuycancel": { + "value1": "Cancelling Buy {pair}", + "value2": "limit {limit:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhooksell": { + "value1": "Selling {pair}", + "value2": "limit {limit:8f}", + "value3": "profit: {profit_amount:8f} {stake_currency}" + }, + "webhooksellcancel": { + "value1": "Cancelling Sell {pair}", + "value2": "limit {limit:8f}", + "value3": "profit: {profit_amount:8f} {stake_currency}" + }, + "webhookstatus": { + "value1": "Status: {status}", + "value2": "", + "value3": "" + } + } def test__init__(mocker, default_conf): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f334e4eb0..429d3599d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -300,7 +300,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf # stoploss shoud be hit assert freqtrade.handle_trade(trade) is True - assert log_has('executed sell, reason: SellType.STOP_LOSS', caplog) + assert log_has('Executed Sell for NEO/BTC. Reason: SellType.STOP_LOSS', caplog) assert trade.sell_reason == SellType.STOP_LOSS.value @@ -1964,7 +1964,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 0 - assert log_has_re("Buy order canceled on Exchange for Trade.*", caplog) + assert log_has_re("Buy order cancelled on exchange for Trade.*", caplog) def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, open_trade, @@ -2045,7 +2045,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 assert open_trade.is_open is True - assert log_has_re("Sell order canceled on exchange for Trade.*", caplog) + assert log_has_re("Sell order cancelled on exchange for Trade.*", caplog) def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,