diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index c7f9c58f6..54e6f50cb 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -216,11 +216,14 @@ Once all positions are sold, run `/stop` to completely stop the bot. ### /status For each open trade, the bot will send you the following message. +Enter Tag is configurable via Strategy. > **Trade ID:** `123` `(since 1 days ago)` > **Current Pair:** CVC/BTC -> **Open Since:** `1 days ago` +> **Direction:** Long +> **Leverage:** 1.0 > **Amount:** `26.64180098` +> **Enter Tag:** Awesome Long Signal > **Open Rate:** `0.00007489` > **Current Rate:** `0.00007489` > **Current Profit:** `12.95%` @@ -231,10 +234,10 @@ For each open trade, the bot will send you the following message. Return the status of all open trades in a table format. ``` - ID Pair Since Profit ----- -------- ------- -------- - 67 SC/BTC 1 d 13.33% - 123 CVC/BTC 1 h 12.95% +ID L/S Pair Since Profit +---- -------- ------- -------- + 67 L SC/BTC 1 d 13.33% + 123 S CVC/BTC 1 h 12.95% ``` ### /count diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 6ee01a615..fe68a5ae7 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -98,12 +98,14 @@ Different payloads can be configured for different events. Not all fields are ne ### Webhookbuy -The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format. +The fields in `webhook.webhookbuy` are filled when the bot executes a long/short. Parameters are filled using string.format. Possible parameters are: * `trade_id` * `exchange` * `pair` +* `direction` +* `leverage` * ~~`limit` # Deprecated - should no longer be used.~~ * `open_rate` * `amount` @@ -117,12 +119,14 @@ Possible parameters are: ### Webhookbuycancel -The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format. +The fields in `webhook.webhookbuycancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format. Possible parameters are: * `trade_id` * `exchange` * `pair` +* `direction` +* `leverage` * `limit` * `amount` * `open_date` @@ -135,12 +139,14 @@ Possible parameters are: ### Webhookbuyfill -The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format. +The fields in `webhook.webhookbuyfill` are filled when the bot filled a long/short order. Parameters are filled using string.format. Possible parameters are: * `trade_id` * `exchange` * `pair` +* `direction` +* `leverage` * `open_rate` * `amount` * `open_date` @@ -152,13 +158,14 @@ Possible parameters are: * `enter_tag` ### Webhooksell - The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. Possible parameters are: * `trade_id` * `exchange` * `pair` +* `direction` +* `leverage` * `gain` * `limit` * `amount` @@ -180,6 +187,8 @@ Possible parameters are: * `trade_id` * `exchange` * `pair` +* `direction` +* `leverage` * `gain` * `close_rate` * `amount` @@ -202,6 +211,8 @@ Possible parameters are: * `trade_id` * `exchange` * `pair` +* `direction` +* `leverage` * `gain` * `limit` * `amount` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d953f77ae..8f3357373 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -775,6 +775,8 @@ class FreqtradeBot(LoggingMixin): 'enter_tag': trade.enter_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, + 'leverage': trade.leverage if trade.leverage else None, + 'direction': 'Short' if trade.is_short else 'Long', 'limit': trade.open_rate, # Deprecated (?) 'open_rate': trade.open_rate, 'order_type': order_type, @@ -802,6 +804,8 @@ class FreqtradeBot(LoggingMixin): 'enter_tag': trade.enter_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, + 'leverage': trade.leverage, + 'direction': 'Short' if trade.is_short else 'Long', 'limit': trade.open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, @@ -1376,6 +1380,8 @@ class FreqtradeBot(LoggingMixin): 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, + 'leverage': trade.leverage, + 'direction': 'Short' if trade.is_short else 'Long', 'gain': gain, 'limit': profit_rate, 'order_type': order_type, @@ -1422,6 +1428,8 @@ class FreqtradeBot(LoggingMixin): 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, + 'leverage': trade.leverage, + 'direction': 'Short' if trade.is_short else 'Long', 'gain': gain, 'limit': profit_rate or 0, 'order_type': order_type, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f11243208..129248416 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -233,6 +233,7 @@ class RPC: current_rate = NAN trade_profit = trade.calc_profit(current_rate) profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' + direction_str = 'S' if trade.is_short else 'L' if self._fiat_converter: fiat_profit = self._fiat_converter.convert_amount( trade_profit, @@ -244,7 +245,7 @@ class RPC: fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ else fiat_profit_sum + fiat_profit trades_list.append([ - trade.id, + f'{trade.id} {direction_str}', trade.pair + ('*' if (trade.open_order_id is not None and trade.close_rate_requested is None) else '') + ('**' if (trade.close_rate_requested is not None) else ''), @@ -255,7 +256,7 @@ class RPC: if self._fiat_converter: profitcol += " (" + fiat_display_currency + ")" - columns = ['ID', 'Pair', 'Since', profitcol] + columns = ['ID L/S', 'Pair', 'Since', profitcol] return trades_list, columns, fiat_profit_sum def _rpc_daily_profit( diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b394fad11..a47206d36 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -221,20 +221,25 @@ class Telegram(RPCHandler): msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) else: msg['stake_amount_fiat'] = 0 - is_fill = msg['type'] == RPCMessageType.BUY_FILL + is_fill = msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL] emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}' + enter_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['type'] + in [RPCMessageType.BUY_FILL, RPCMessageType.BUY] + else {'enter': 'Short', 'entered': 'Shorted'}) message = ( - f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}" + f"{emoji} *{msg['exchange']}:*" + f" {enter_side['entered'] if is_fill else enter_side['enter']} {msg['pair']}" f" (#{msg['trade_id']})\n" ) message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else "" message += f"*Amount:* `{msg['amount']:.8f}`\n" + if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: + message += f"*Leverage:* `{msg['leverage']}`\n" - if msg['type'] == RPCMessageType.BUY_FILL: + if msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]: message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n" - - elif msg['type'] == RPCMessageType.BUY: + elif msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]: message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\ f"*Current Rate:* `{msg['current_rate']:.8f}`\n" @@ -255,6 +260,9 @@ class Telegram(RPCHandler): msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None msg['emoji'] = self._get_sell_emoji(msg) + msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n" + if msg.get('leverage', None) and msg.get('leverage', 1.0) != 1.0 + else "") # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell @@ -270,15 +278,17 @@ class Telegram(RPCHandler): is_fill = msg['type'] == RPCMessageType.SELL_FILL message = ( f"{msg['emoji']} *{msg['exchange']}:* " - f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n" + f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" f"*{'Profit' if is_fill else 'Unrealized Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"*Enter Tag:* `{msg['enter_tag']}`\n" - f"*Sell Reason:* `{msg['sell_reason']}`\n" + f"*Exit Reason:* `{msg['sell_reason']}`\n" f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n" + f"*Direction:* `{msg['direction']}`\n" + f"{msg['leverage_text']}" f"*Amount:* `{msg['amount']:.8f}`\n" - f"*Open Rate:* `{msg['open_rate']:.8f}`\n") - + f"*Open Rate:* `{msg['open_rate']:.8f}`\n" + ) if msg['type'] == RPCMessageType.SELL: message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n" f"*Close Rate:* `{msg['limit']:.8f}`") @@ -289,16 +299,19 @@ class Telegram(RPCHandler): return message def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: - if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL]: + if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL, RPCMessageType.SHORT, + RPCMessageType.SHORT_FILL]: message = self._format_buy_msg(msg) elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]: message = self._format_sell_msg(msg) - elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): - msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell' + elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL, + RPCMessageType.SELL_CANCEL): + msg['message_side'] = 'enter' if msg_type in [RPCMessageType.BUY_CANCEL, + RPCMessageType.SHORT_CANCEL] else 'exit' message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling open {message_side} Order for {pair} (#{trade_id}). " + "Cancelling {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) elif msg_type == RPCMessageType.PROTECTION_TRIGGER: @@ -398,6 +411,8 @@ class Telegram(RPCHandler): lines = [ "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", + "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"), + "*Leverage:* `{leverage}`" if r.get('leverage') else "", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", "*Open Rate:* `{open_rate:.8f}`", diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 58b75769e..b0a884a88 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -44,11 +44,11 @@ class Webhook(RPCHandler): """ Send a message to telegram channel """ try: - if msg['type'] == RPCMessageType.BUY: + if msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]: valuedict = self._config['webhook'].get('webhookbuy', None) - elif msg['type'] == RPCMessageType.BUY_CANCEL: + elif msg['type'] in [RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL]: valuedict = self._config['webhook'].get('webhookbuycancel', None) - elif msg['type'] == RPCMessageType.BUY_FILL: + elif msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]: valuedict = self._config['webhook'].get('webhookbuyfill', None) elif msg['type'] == RPCMessageType.SELL: valuedict = self._config['webhook'].get('webhooksell', None) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f164d28b0..55a209b6a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -202,7 +202,8 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'stoploss_current_dist_ratio': -0.0002, 'stop_loss_ratio': -0.0001, 'open_order': '(limit buy rem=0.00000000)', - 'is_open': True + 'is_open': True, + 'is_short': False }]), ) @@ -316,7 +317,8 @@ 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 'ETH/BTC' in fields[1] + assert 'L' in fields[1] + assert 'ETH/BTC' in fields[2] assert msg_mock.call_count == 1 @@ -946,11 +948,13 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', + 'leverage': 1.0, 'limit': 1.173e-05, 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.173e-05, + 'direction': 'Long', 'profit_amount': 6.314e-05, 'profit_ratio': 0.0629778, 'stake_currency': 'BTC', @@ -1011,11 +1015,13 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', + 'leverage': 1.0, 'limit': 1.043e-05, 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.043e-05, + 'direction': 'Long', 'profit_amount': -5.497e-05, 'profit_ratio': -0.05482878, 'stake_currency': 'BTC', @@ -1066,11 +1072,13 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', + 'leverage': 1.0, 'limit': 1.099e-05, 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.099e-05, + 'direction': 'Long', 'profit_amount': -4.09e-06, 'profit_ratio': -0.00408133, 'stake_currency': 'BTC', @@ -1643,14 +1651,21 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] -def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: +@pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [ + (RPCMessageType.BUY, 'Long', 'long_signal_01', None), + (RPCMessageType.BUY, 'Long', 'long_signal_01', 1.0), + (RPCMessageType.BUY, 'Long', 'long_signal_01', 5.0), + (RPCMessageType.SHORT, 'Short', 'short_signal_01', 2.0)]) +def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, + enter, enter_signal, leverage) -> None: msg = { - 'type': RPCMessageType.BUY, + 'type': message_type, 'trade_id': 1, - 'enter_tag': 'buy_signal_01', + 'enter_tag': enter_signal, 'exchange': 'Binance', 'pair': 'ETH/BTC', + 'leverage': leverage, 'limit': 1.099e-05, 'order_type': 'limit', 'stake_amount': 0.001, @@ -1664,13 +1679,17 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg(msg) - assert msg_mock.call_args[0][0] \ - == '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \ - '*Enter Tag:* `buy_signal_01`\n' \ - '*Amount:* `1333.33333333`\n' \ - '*Open Rate:* `0.00001099`\n' \ - '*Current Rate:* `0.00001099`\n' \ - '*Total:* `(0.00100000 BTC, 12.345 USD)`' + leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' + + assert msg_mock.call_args[0][0] == ( + f'\N{LARGE BLUE CIRCLE} *Binance:* {enter} ETH/BTC (#1)\n' + f'*Enter Tag:* `{enter_signal}`\n' + '*Amount:* `1333.33333333`\n' + f'{leverage_text}' + '*Open Rate:* `0.00001099`\n' + '*Current Rate:* `0.00001099`\n' + '*Total:* `(0.00100000 BTC, 12.345 USD)`' + ) freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} caplog.clear() @@ -1688,20 +1707,23 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg_mock.call_args_list[0][1]['disable_notification'] is True -def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: +@pytest.mark.parametrize('message_type,enter_signal', [ + (RPCMessageType.BUY_CANCEL, 'long_signal_01'), + (RPCMessageType.SHORT_CANCEL, 'short_signal_01')]) +def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_CANCEL, - 'enter_tag': 'buy_signal_01', + 'type': message_type, + 'enter_tag': enter_signal, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'reason': CANCEL_REASON['TIMEOUT'] }) assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* ' - 'Cancelling open buy Order for ETH/BTC (#1). ' + 'Cancelling enter Order for ETH/BTC (#1). ' 'Reason: cancelled due to timeout.') @@ -1733,17 +1755,24 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> "*All pairs* will be locked until `2021-09-01 06:45:00`.") -def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: +@pytest.mark.parametrize('message_type,entered,enter_signal,leverage', [ + (RPCMessageType.BUY_FILL, 'Longed', 'long_signal_01', 1.0), + (RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0), + (RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0), + ]) +def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered, + enter_signal, leverage) -> 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, + 'type': message_type, 'trade_id': 1, - 'enter_tag': 'buy_signal_01', + 'enter_tag': enter_signal, 'exchange': 'Binance', 'pair': 'ETH/BTC', + 'leverage': leverage, 'stake_amount': 0.001, # 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', @@ -1752,13 +1781,15 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - - assert msg_mock.call_args[0][0] \ - == '\N{CHECK MARK} *Binance:* Bought ETH/BTC (#1)\n' \ - '*Enter Tag:* `buy_signal_01`\n' \ - '*Amount:* `1333.33333333`\n' \ - '*Open Rate:* `0.00001099`\n' \ - '*Total:* `(0.00100000 BTC, 12.345 USD)`' + leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else '' + assert msg_mock.call_args[0][0] == ( + f'\N{CHECK MARK} *Binance:* {entered} ETH/BTC (#1)\n' + f'*Enter Tag:* `{enter_signal}`\n' + '*Amount:* `1333.33333333`\n' + f"{leverage_text}" + '*Open Rate:* `0.00001099`\n' + '*Total:* `(0.00100000 BTC, 12.345 USD)`' + ) def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1772,6 +1803,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', + 'leverage': 1.0, + 'direction': 'Long', 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, @@ -1787,17 +1820,18 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-1), 'close_date': arrow.utcnow(), }) - assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' - '*Enter Tag:* `buy_signal1`\n' - '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1:00:00 (60.0 min)`\n' - '*Amount:* `1333.33333333`\n' - '*Open Rate:* `0.00007500`\n' - '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' - ) + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n' + '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' + '*Enter Tag:* `buy_signal1`\n' + '*Exit Reason:* `stop_loss`\n' + '*Duration:* `1:00:00 (60.0 min)`\n' + '*Direction:* `Long`\n' + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' + '*Current Rate:* `0.00003201`\n' + '*Close Rate:* `0.00003201`' + ) msg_mock.reset_mock() telegram.send_msg({ @@ -1805,6 +1839,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', + 'direction': 'Long', 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, @@ -1819,17 +1854,18 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'close_date': arrow.utcnow(), }) - assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41%`\n' - '*Enter Tag:* `buy_signal1`\n' - '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' - '*Amount:* `1333.33333333`\n' - '*Open Rate:* `0.00007500`\n' - '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' - ) + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n' + '*Unrealized Profit:* `-57.41%`\n' + '*Enter Tag:* `buy_signal1`\n' + '*Exit Reason:* `stop_loss`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' + '*Direction:* `Long`\n' + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' + '*Current Rate:* `0.00003201`\n' + '*Close Rate:* `0.00003201`' + ) # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount @@ -1847,9 +1883,9 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'pair': 'KEY/ETH', 'reason': 'Cancelled on exchange' }) - assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' - ' Reason: Cancelled on exchange.') + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance:* Cancelling exit Order for KEY/ETH (#1).' + ' Reason: Cancelled on exchange.') msg_mock.reset_mock() telegram.send_msg({ @@ -1859,14 +1895,19 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'pair': 'KEY/ETH', 'reason': 'timeout' }) - assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' - ' Reason: timeout.') + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance:* Cancelling exit Order for KEY/ETH (#1). Reason: timeout.') # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount -def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: +@pytest.mark.parametrize('direction,enter_signal,leverage', [ + ('Long', 'long_signal_01', None), + ('Long', 'long_signal_01', 1.0), + ('Long', 'long_signal_01', 5.0), + ('Short', 'short_signal_01', 2.0)]) +def test_send_msg_sell_fill_notification(default_conf, mocker, direction, + enter_signal, leverage) -> None: default_conf['telegram']['notification_settings']['sell_fill'] = 'on' telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1876,6 +1917,8 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', + 'leverage': leverage, + 'direction': direction, 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, @@ -1885,21 +1928,25 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: 'profit_amount': -0.05746268, 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', - 'enter_tag': 'buy_signal1', + 'enter_tag': enter_signal, 'sell_reason': SellType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'close_date': arrow.utcnow(), }) - assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Sold KEY/ETH (#1)\n' - '*Profit:* `-57.41%`\n' - '*Enter Tag:* `buy_signal1`\n' - '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' - '*Amount:* `1333.33333333`\n' - '*Open Rate:* `0.00007500`\n' - '*Close Rate:* `0.00003201`' - ) + + leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance:* Exited KEY/ETH (#1)\n' + '*Profit:* `-57.41%`\n' + f'*Enter Tag:* `{enter_signal}`\n' + '*Exit Reason:* `stop_loss`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' + f"*Direction:* `{direction}`\n" + f"{leverage_text}" + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' + '*Close Rate:* `0.00003201`' + ) def test_send_msg_status_notification(default_conf, mocker) -> None: @@ -1938,16 +1985,22 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None: }) -def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: +@pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [ + (RPCMessageType.BUY, 'Long', 'long_signal_01', None), + (RPCMessageType.BUY, 'Long', 'long_signal_01', 2.0), + (RPCMessageType.SHORT, 'Short', 'short_signal_01', 2.0)]) +def test_send_msg_buy_notification_no_fiat( + default_conf, mocker, message_type, enter, enter_signal, leverage) -> None: del default_conf['fiat_display_currency'] telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY, - 'enter_tag': 'buy_signal_01', + 'type': message_type, + 'enter_tag': enter_signal, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', + 'leverage': leverage, 'limit': 1.099e-05, 'order_type': 'limit', 'stake_amount': 0.001, @@ -1958,15 +2011,27 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' - '*Enter Tag:* `buy_signal_01`\n' - '*Amount:* `1333.33333333`\n' - '*Open Rate:* `0.00001099`\n' - '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.00100000 BTC)`') + + leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' + assert msg_mock.call_args[0][0] == ( + f'\N{LARGE BLUE CIRCLE} *Binance:* {enter} ETH/BTC (#1)\n' + f'*Enter Tag:* `{enter_signal}`\n' + '*Amount:* `1333.33333333`\n' + f'{leverage_text}' + '*Open Rate:* `0.00001099`\n' + '*Current Rate:* `0.00001099`\n' + '*Total:* `(0.00100000 BTC)`' + ) -def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: +@pytest.mark.parametrize('direction,enter_signal,leverage', [ + ('Long', 'long_signal_01', None), + ('Long', 'long_signal_01', 1.0), + ('Long', 'long_signal_01', 5.0), + ('Short', 'short_signal_01', 2.0), + ]) +def test_send_msg_sell_notification_no_fiat( + default_conf, mocker, direction, enter_signal, leverage) -> None: del default_conf['fiat_display_currency'] telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1976,6 +2041,8 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', + 'leverage': leverage, + 'direction': direction, 'limit': 3.201e-05, 'amount': 1333.3333333333335, 'order_type': 'limit', @@ -1985,21 +2052,26 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', 'fiat_currency': 'USD', - 'enter_tag': 'buy_signal1', + 'enter_tag': enter_signal, 'sell_reason': SellType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'close_date': arrow.utcnow(), }) - assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41%`\n' - '*Enter Tag:* `buy_signal1`\n' - '*Sell Reason:* `stop_loss`\n' - '*Duration:* `2:35:03 (155.1 min)`\n' - '*Amount:* `1333.33333333`\n' - '*Open Rate:* `0.00007500`\n' - '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' - ) + + leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n' + '*Unrealized Profit:* `-57.41%`\n' + f'*Enter Tag:* `{enter_signal}`\n' + '*Exit Reason:* `stop_loss`\n' + '*Duration:* `2:35:03 (155.1 min)`\n' + f'*Direction:* `{direction}`\n' + f'{leverage_text}' + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' + '*Current Rate:* `0.00003201`\n' + '*Close Rate:* `0.00003201`' + ) @pytest.mark.parametrize('msg,expected', [ diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 17d1baca9..69a2d79fb 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -18,17 +18,23 @@ def get_webhook_dict() -> dict: "webhookbuy": { "value1": "Buying {pair}", "value2": "limit {limit:8f}", - "value3": "{stake_amount:8f} {stake_currency}" + "value3": "{stake_amount:8f} {stake_currency}", + "value4": "leverage {leverage:.1f}", + "value5": "direction {direction}" }, "webhookbuycancel": { "value1": "Cancelling Open Buy Order for {pair}", "value2": "limit {limit:8f}", - "value3": "{stake_amount:8f} {stake_currency}" + "value3": "{stake_amount:8f} {stake_currency}", + "value4": "leverage {leverage:.1f}", + "value5": "direction {direction}" }, "webhookbuyfill": { "value1": "Buy Order for {pair} filled", "value2": "at {open_rate:8f}", - "value3": "{stake_amount:8f} {stake_currency}" + "value3": "{stake_amount:8f} {stake_currency}", + "value4": "leverage {leverage:.1f}", + "value5": "direction {direction}" }, "webhooksell": { "value1": "Selling {pair}", @@ -71,6 +77,8 @@ def test_send_msg_webhook(default_conf, mocker): 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', + 'leverage': 1.0, + 'direction': 'Long', 'limit': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, @@ -85,6 +93,37 @@ def test_send_msg_webhook(default_conf, mocker): default_conf["webhook"]["webhookbuy"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) + assert (msg_mock.call_args[0][0]["value4"] == + default_conf["webhook"]["webhookbuy"]["value4"].format(**msg)) + assert (msg_mock.call_args[0][0]["value5"] == + default_conf["webhook"]["webhookbuy"]["value5"].format(**msg)) + # Test short + msg_mock.reset_mock() + + msg = { + 'type': RPCMessageType.SHORT, + 'exchange': 'Binance', + 'pair': 'ETH/BTC', + 'leverage': 2.0, + 'direction': 'Short', + 'limit': 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"]["webhookbuy"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookbuy"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) + assert (msg_mock.call_args[0][0]["value4"] == + default_conf["webhook"]["webhookbuy"]["value4"].format(**msg)) + assert (msg_mock.call_args[0][0]["value5"] == + default_conf["webhook"]["webhookbuy"]["value5"].format(**msg)) # Test buy cancel msg_mock.reset_mock() @@ -92,6 +131,8 @@ def test_send_msg_webhook(default_conf, mocker): 'type': RPCMessageType.BUY_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', + 'leverage': 1.0, + 'direction': 'Long', 'limit': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, @@ -106,6 +147,33 @@ def test_send_msg_webhook(default_conf, mocker): default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) + # Test short cancel + msg_mock.reset_mock() + + msg = { + 'type': RPCMessageType.SHORT_CANCEL, + 'exchange': 'Binance', + 'pair': 'ETH/BTC', + 'leverage': 2.0, + 'direction': 'Short', + 'limit': 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"]["webhookbuycancel"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) + assert (msg_mock.call_args[0][0]["value4"] == + default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) + assert (msg_mock.call_args[0][0]["value5"] == + default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) # Test buy fill msg_mock.reset_mock() @@ -113,6 +181,8 @@ def test_send_msg_webhook(default_conf, mocker): 'type': RPCMessageType.BUY_FILL, 'exchange': 'Binance', 'pair': 'ETH/BTC', + 'leverage': 1.0, + 'direction': 'Long', 'open_rate': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, @@ -127,8 +197,40 @@ def test_send_msg_webhook(default_conf, mocker): default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) + assert (msg_mock.call_args[0][0]["value4"] == + default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) + assert (msg_mock.call_args[0][0]["value5"] == + default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) + # Test short fill + msg_mock.reset_mock() + + msg = { + 'type': RPCMessageType.SHORT_FILL, + 'exchange': 'Binance', + 'pair': 'ETH/BTC', + 'leverage': 2.0, + 'direction': 'Short', + '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)) + assert (msg_mock.call_args[0][0]["value4"] == + default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) + assert (msg_mock.call_args[0][0]["value5"] == + default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) # Test sell msg_mock.reset_mock() + msg = { 'type': RPCMessageType.SELL, 'exchange': 'Binance', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1a1384442..a99ccc33e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2874,6 +2874,8 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'amount': amt, 'order_type': 'limit', 'buy_tag': None, + 'direction': 'Short' if trade.is_short else 'Long', + 'leverage': 1.0, 'enter_tag': None, 'open_rate': open_rate, 'current_rate': 2.01 if is_short else 2.3, @@ -2926,6 +2928,8 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/USDT', + 'direction': 'Short' if trade.is_short else 'Long', + 'leverage': 1.0, 'gain': 'loss', 'limit': 2.2 if is_short else 2.01, 'amount': 29.70297029 if is_short else 30.0, @@ -3004,6 +3008,8 @@ def test_execute_trade_exit_custom_exit_price( 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/USDT', + 'direction': 'Short' if trade.is_short else 'Long', + 'leverage': 1.0, 'gain': profit_or_loss, 'limit': limit, 'amount': amount, @@ -3069,6 +3075,8 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/USDT', + 'direction': 'Short' if trade.is_short else 'Long', + 'leverage': 1.0, 'gain': 'loss', 'limit': 2.02 if is_short else 1.98, 'amount': 29.70297029 if is_short else 30.0, @@ -3182,6 +3190,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange( assert rpc_mock.call_count == 3 +# TODO-lev: add short, RPC short, short fill def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt, ticker_usdt, fee, mocker) -> None: default_conf_usdt['exchange']['name'] = 'binance' @@ -3323,6 +3332,8 @@ def test_execute_trade_exit_market_order( 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/USDT', + 'direction': 'Short' if trade.is_short else 'Long', + 'leverage': 1.0, 'gain': profit_or_loss, 'limit': limit, 'amount': round(amount, 9),