Merge pull request #9611 from freqtrade/rpc_final_exit_msg

Improve Telegram final exit msg
This commit is contained in:
Matthias
2024-01-06 15:46:56 +01:00
committed by GitHub
5 changed files with 108 additions and 92 deletions

View File

@@ -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)

View File

@@ -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):

View File

@@ -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"
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}"

View File

@@ -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:

View File

@@ -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