diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d344cf652..8709e1b4d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1804,7 +1804,7 @@ class FreqtradeBot(LoggingMixin): 'open_rate': trade.open_rate, 'close_rate': order_rate, 'current_rate': current_rate, - 'profit_amount': profit.profit_abs if fill else profit.total_profit, + 'profit_amount': profit.profit_abs, 'profit_ratio': profit.profit_ratio, 'buy_tag': trade.enter_tag, 'enter_tag': trade.enter_tag, @@ -1817,6 +1817,8 @@ class FreqtradeBot(LoggingMixin): 'fiat_currency': self.config.get('fiat_display_currency'), 'sub_trade': sub_trade, 'cumulative_profit': trade.realized_profit, + 'final_profit_ratio': trade.close_profit if not trade.is_open else None, + 'is_final_exit': trade.is_open is False, } # Send the message @@ -1970,15 +1972,16 @@ class FreqtradeBot(LoggingMixin): self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool): """send "fill" notifications""" - sub_trade = not isclose(order.safe_amount_after_fee, - trade.amount, abs_tol=constants.MATH_CLOSE_PREC) if order.ft_order_side == trade.exit_side: # Exit notification if send_msg and not stoploss_order and order.order_id not in trade.open_orders_ids: - self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order) + self._notify_exit(trade, order.order_type, fill=True, + sub_trade=trade.is_open, order=order) if not trade.is_open: self.handle_protections(trade.pair, trade.trade_direction) elif send_msg and order.order_id not in trade.open_orders_ids and not stoploss_order: + sub_trade = not isclose(order.safe_amount_after_fee, + trade.amount, abs_tol=constants.MATH_CLOSE_PREC) # Enter fill self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade) diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index bafe653bc..d69c39764 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -85,6 +85,8 @@ class RPCExitMsg(__RPCEntryExitMsgBase): close_date: datetime # current_rate: Optional[float] order_rate: Optional[float] + final_profit_ratio: Optional[float] + is_final_exit: bool class RPCExitCancelMsg(__RPCEntryExitMsgBase): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 63785b1ef..51c27302b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -329,11 +329,7 @@ class Telegram(RPCHandler): return '' def _format_entry_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 + is_fill = msg['type'] in [RPCMessageType.ENTRY_FILL] emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}' @@ -348,7 +344,7 @@ class Telegram(RPCHandler): message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') 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" + message += f"*Leverage:* `{msg['leverage']:.1g}`\n" if msg['type'] in [RPCMessageType.ENTRY_FILL]: message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n" @@ -356,95 +352,93 @@ class Telegram(RPCHandler): message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"\ f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" + profit_fiat_extra = self.__format_profit_fiat(msg, 'stake_amount') + total = round_coin_value(msg['stake_amount'], msg['stake_currency']) - if msg.get('fiat_currency'): - message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + message += f"*Total:* `{total}{profit_fiat_extra}`" - message += ")`" return message def _format_exit_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( + duration = msg['close_date'].replace( microsecond=0) - msg['open_date'].replace(microsecond=0) - msg['duration_min'] = msg['duration'].total_seconds() / 60 + duration_min = duration.total_seconds() / 60 - 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') and msg.get('leverage', 1.0) != 1.0 - else "") + leverage_text = (f"*Leverage:* `{msg['leverage']:.1g}`\n" + if msg.get('leverage') 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 /forceexit - 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']) - msg['profit_extra'] = f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}" - else: - msg['profit_extra'] = '' - msg['profit_extra'] = ( + profit_fiat_extra = self.__format_profit_fiat(msg, 'profit_amount') + + profit_extra = ( f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" - f"{msg['profit_extra']})") + f"{profit_fiat_extra})") is_fill = msg['type'] == RPCMessageType.EXIT_FILL is_sub_trade = msg.get('sub_trade') is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit') - profit_prefix = ('Sub ' if is_sub_profit else 'Cumulative ') if is_sub_trade else '' + is_final_exit = msg.get('is_final_exit', False) and is_sub_profit + profit_prefix = 'Sub ' if is_sub_trade else '' cp_extra = '' exit_wording = 'Exited' if is_fill else 'Exiting' - if is_sub_profit and is_sub_trade: - if self._rpc._fiat_converter: - cp_fiat = self._rpc._fiat_converter.convert_amount( - msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency']) - cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}" - exit_wording = f"Partially {exit_wording.lower()}" - cp_extra = ( - f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " - f"{msg['stake_currency']}{cp_extra}`)\n" - ) + if is_sub_trade or is_final_exit: + cp_fiat = self.__format_profit_fiat(msg, 'cumulative_profit') + if is_final_exit: + profit_prefix = 'Sub ' + cp_extra = ( + f"*Final Profit:* `{msg['final_profit_ratio']:.2%} " + f"({msg['cumulative_profit']:.8f} {msg['stake_currency']}{cp_fiat})`\n" + ) + else: + exit_wording = f"Partially {exit_wording.lower()}" + if msg['cumulative_profit']: + cp_extra = ( + f"*Cumulative Profit:* `{msg['cumulative_profit']:.8f} " + f"{msg['stake_currency']}{cp_fiat}`\n" + ) + enter_tag = f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else "" message = ( - f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " + f"{self._get_exit_emoji(msg)} *{self._exchange_from_msg(msg)}:* " f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n" f"{self._add_analyzed_candle(msg['pair'])}" f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " - f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" + f"`{msg['profit_ratio']:.2%}{profit_extra}`\n" f"{cp_extra}" - f"*Enter Tag:* `{msg['enter_tag']}`\n" + f"{enter_tag}" f"*Exit Reason:* `{msg['exit_reason']}`\n" f"*Direction:* `{msg['direction']}`\n" - f"{msg['leverage_text']}" - f"*Amount:* `{msg['amount']:.8f}`\n" + f"{leverage_text}" + f"*Amount:* `{round(msg['amount'], 8):.8f}`\n" f"*Open Rate:* `{msg['open_rate']:.8f}`\n" ) if msg['type'] == RPCMessageType.EXIT: message += f"*Current Rate:* `{msg['current_rate']:.8f}`\n" if msg['order_rate']: message += f"*Exit Rate:* `{msg['order_rate']:.8f}`" - elif msg['type'] == RPCMessageType.EXIT_FILL: message += f"*Exit Rate:* `{msg['close_rate']:.8f}`" + if is_sub_trade: - 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 + stake_amount_fiat = self.__format_profit_fiat(msg, 'stake_amount') + rem = round_coin_value(msg['stake_amount'], msg['stake_currency']) - message += f"\n*Remaining:* `({rem}" - - if msg.get('fiat_currency', None): - message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" - - message += ")`" + message += f"\n*Remaining:* `{rem}{stake_amount_fiat}`" else: - message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`" + message += f"\n*Duration:* `{duration} ({duration_min:.1f} min)`" return message + def __format_profit_fiat(self, msg: Dict[str, Any], key: str) -> str: + """ + Format Fiat currency to append to regular profit output + """ + profit_fiat_extra = '' + if self._rpc._fiat_converter and (fiat_currency := msg.get('fiat_currency')): + profit_fiat = self._rpc._fiat_converter.convert_amount( + msg[key], msg['stake_currency'], fiat_currency) + profit_fiat_extra = f" / {profit_fiat:.3f} {fiat_currency}" + return profit_fiat_extra + def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> Optional[str]: if msg_type in [RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]: message = self._format_entry_msg(msg) @@ -519,14 +513,14 @@ class Telegram(RPCHandler): self._send_msg(message, disable_notification=(noti == 'silent')), self._loop) - def _get_sell_emoji(self, msg): + def _get_exit_emoji(self, msg): """ - Get emoji for sell-side + Get emoji for exit-messages """ - if float(msg['profit_percent']) >= 5.0: + if float(msg['profit_ratio']) >= 0.05: return "\N{ROCKET}" - elif float(msg['profit_percent']) >= 0.0: + elif float(msg['profit_ratio']) >= 0.0: return "\N{EIGHT SPOKED ASTERISK}" elif msg['exit_reason'] == "stop_loss": return "\N{WARNING SIGN}" diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index fccec12f0..59a531c27 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1165,6 +1165,8 @@ async def test_telegram_forceexit_handle(default_conf, update, ticker, fee, 'stake_amount': 0.0009999999999054, 'sub_trade': False, 'cumulative_profit': 0.0, + 'is_final_exit': False, + 'final_profit_ratio': None, } == last_msg @@ -1237,6 +1239,8 @@ async def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee 'stake_amount': 0.0009999999999054, 'sub_trade': False, 'cumulative_profit': 0.0, + 'is_final_exit': False, + 'final_profit_ratio': None, } == last_msg @@ -1299,6 +1303,8 @@ async def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) - 'stake_amount': 0.0009999999999054, 'sub_trade': False, 'cumulative_profit': 0.0, + 'is_final_exit': False, + 'final_profit_ratio': None, } == msg @@ -2005,7 +2011,7 @@ def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type, telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg(msg) - leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' + leverage_text = f'*Leverage:* `{leverage:.1g}`\n' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n' @@ -2015,7 +2021,7 @@ def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type, f'{leverage_text}' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.01465333 BTC, 180.895 USD)`' + '*Total:* `0.01465333 BTC / 180.895 USD`' ) freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} @@ -2109,14 +2115,14 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en 'amount': 1333.3333333333335, 'open_date': dt_now() - timedelta(hours=1) }) - leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else '' + leverage_text = f'*Leverage:* `{leverage:.1g}`\n' if leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( f'\N{CHECK MARK} *Binance (dry):* {entered}ed ETH/BTC (#1)\n' f'*Enter Tag:* `{enter_signal}`\n' '*Amount:* `1333.33333333`\n' f"{leverage_text}" '*Open Rate:* `0.00001099`\n' - '*Total:* `(0.01465333 BTC, 180.895 USD)`' + '*Total:* `0.01465333 BTC / 180.895 USD`' ) msg_mock.reset_mock() @@ -2143,11 +2149,11 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en '*Amount:* `1333.33333333`\n' f"{leverage_text}" '*Open Rate:* `0.00001099`\n' - '*Total:* `(0.01465333 BTC, 180.895 USD)`' + '*Total:* `0.01465333 BTC / 180.895 USD`' ) -def test_send_msg_sell_notification(default_conf, mocker) -> None: +def test_send_msg_exit_notification(default_conf, mocker) -> None: with time_machine.travel("2022-09-01 05:00:00 +00:00", tick=False): telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -2217,7 +2223,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Partially exiting KEY/ETH (#1)\n' '*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' - '*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n' + '*Cumulative Profit:* `-0.15746268 ETH / -24.812 USD`\n' '*Enter Tag:* `buy_signal1`\n' '*Exit Reason:* `stop_loss`\n' '*Direction:* `Long`\n' @@ -2225,7 +2231,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Exit Rate:* `0.00003201`\n' - '*Remaining:* `(0.01 ETH, -24.812 USD)`' + '*Remaining:* `0.01 ETH / -24.812 USD`' ) msg_mock.reset_mock() @@ -2244,6 +2250,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'profit_amount': -0.05746268, 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', + 'fiat_currency': None, 'enter_tag': 'buy_signal1', 'exit_reason': ExitType.STOP_LOSS.value, 'open_date': dt_now() - timedelta(days=1, hours=2, minutes=30), @@ -2265,7 +2272,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram._rpc._fiat_converter.convert_amount = old_convamount -async def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: +async def test_send_msg_exit_cancel_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -2303,7 +2310,7 @@ async def test_send_msg_sell_cancel_notification(default_conf, mocker) -> 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, +def test_send_msg_exit_fill_notification(default_conf, mocker, direction, enter_signal, leverage) -> None: default_conf['telegram']['notification_settings']['exit_fill'] = 'on' @@ -2326,13 +2333,14 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction, 'profit_amount': -0.05746268, 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', + 'fiat_currency': None, 'enter_tag': enter_signal, 'exit_reason': ExitType.STOP_LOSS.value, 'open_date': dt_now() - timedelta(days=1, hours=2, minutes=30), 'close_date': dt_now(), }) - leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' + leverage_text = f'*Leverage:* `{leverage:.1g}`\n' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n' '*Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' @@ -2422,7 +2430,7 @@ def test_send_msg_buy_notification_no_fiat( 'open_date': dt_now() - timedelta(hours=1) }) - leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' + leverage_text = f'*Leverage:* `{leverage:.1g}`\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' @@ -2430,7 +2438,7 @@ def test_send_msg_buy_notification_no_fiat( f'{leverage_text}' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.01465333 BTC)`' + '*Total:* `0.01465333 BTC`' ) @@ -2440,7 +2448,7 @@ def test_send_msg_buy_notification_no_fiat( ('Long', 'long_signal_01', 5.0), ('Short', 'short_signal_01', 2.0), ]) -def test_send_msg_sell_notification_no_fiat( +def test_send_msg_exit_notification_no_fiat( default_conf, mocker, direction, enter_signal, leverage, time_machine) -> None: del default_conf['fiat_display_currency'] time_machine.move_to('2022-05-02 00:00:00 +00:00', tick=False) @@ -2469,7 +2477,7 @@ def test_send_msg_sell_notification_no_fiat( 'close_date': dt_now(), }) - leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' + leverage_text = f'*Leverage:* `{leverage:.1g}`\n' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' @@ -2486,20 +2494,20 @@ def test_send_msg_sell_notification_no_fiat( @pytest.mark.parametrize('msg,expected', [ - ({'profit_percent': 20.1, 'exit_reason': 'roi'}, "\N{ROCKET}"), - ({'profit_percent': 5.1, 'exit_reason': 'roi'}, "\N{ROCKET}"), - ({'profit_percent': 2.56, 'exit_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), - ({'profit_percent': 1.0, 'exit_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), - ({'profit_percent': 0.0, 'exit_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), - ({'profit_percent': -5.0, 'exit_reason': 'stop_loss'}, "\N{WARNING SIGN}"), - ({'profit_percent': -2.0, 'exit_reason': 'sell_signal'}, "\N{CROSS MARK}"), + ({'profit_ratio': 0.201, 'exit_reason': 'roi'}, "\N{ROCKET}"), + ({'profit_ratio': 0.051, 'exit_reason': 'roi'}, "\N{ROCKET}"), + ({'profit_ratio': 0.0256, 'exit_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_ratio': 0.01, 'exit_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_ratio': 0.0, 'exit_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_ratio': -0.05, 'exit_reason': 'stop_loss'}, "\N{WARNING SIGN}"), + ({'profit_ratio': -0.02, 'exit_reason': 'sell_signal'}, "\N{CROSS MARK}"), ]) -def test__sell_emoji(default_conf, mocker, msg, expected): +def test__exit_emoji(default_conf, mocker, msg, expected): del default_conf['fiat_display_currency'] telegram, _, _ = get_telegram_testobject(mocker, default_conf) - assert telegram._get_sell_emoji(msg) == expected + assert telegram._get_exit_emoji(msg) == expected async def test_telegram__send_msg(default_conf, mocker, caplog) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f0a87ffdf..bff32c33f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3788,6 +3788,8 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'sub_trade': False, 'cumulative_profit': 0.0, 'stake_amount': pytest.approx(60), + 'is_final_exit': False, + 'final_profit_ratio': None, } == last_msg @@ -3852,6 +3854,8 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'sub_trade': False, 'cumulative_profit': 0.0, 'stake_amount': pytest.approx(60), + 'is_final_exit': False, + 'final_profit_ratio': None, } == last_msg @@ -3937,6 +3941,8 @@ def test_execute_trade_exit_custom_exit_price( 'sub_trade': False, 'cumulative_profit': 0.0, 'stake_amount': pytest.approx(60), + 'is_final_exit': False, + 'final_profit_ratio': None, } == last_msg @@ -4009,6 +4015,8 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'sub_trade': False, 'cumulative_profit': 0.0, 'stake_amount': pytest.approx(60), + 'is_final_exit': False, + 'final_profit_ratio': None, } == last_msg @@ -4274,7 +4282,8 @@ def test_execute_trade_exit_market_order( 'sub_trade': False, 'cumulative_profit': 0.0, 'stake_amount': pytest.approx(60), - + 'is_final_exit': False, + 'final_profit_ratio': None, } == last_msg