From 2fa0503993138c1d17aaa453f2b7bdf78665798f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Nov 2025 20:33:20 +0100 Subject: [PATCH 01/10] fix: telegram crash during exchange downtime --- freqtrade/rpc/telegram.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 69c76b776..3db8724de 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -806,15 +806,14 @@ class Telegram(RPCHandler): if r["is_open"]: if r.get("realized_profit"): - lines.extend( - [ - f"*Realized Profit:* `{r['realized_profit_ratio']:.2%} " - f"({r['realized_profit_r']})`", - ( - f"*Total Profit:* `{r['total_profit_ratio']:.2%} " - f"({r['total_profit_abs_r']})`" - ), - ] + lines.append( + f"*Realized Profit:* `{r['realized_profit_ratio']:.2%} " + f"({r['realized_profit_r']})`" + ) + if r.get("total_profit_ratio"): + lines.append( + f"*Total Profit:* `{r['total_profit_ratio']:.2%} " + f"({r['total_profit_abs_r']})`" ) # Append empty line to improve readability From b92535deea5f11133047f9be99bede9e94bd3ad5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 08:13:48 +0100 Subject: [PATCH 02/10] feat: Add format_pct helper method --- freqtrade/util/__init__.py | 2 ++ freqtrade/util/formatters.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 98fda93cd..de8923389 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -18,6 +18,7 @@ from freqtrade.util.formatters import ( fmt_coin, fmt_coin2, format_duration, + format_pct, round_value, ) from freqtrade.util.ft_precise import FtPrecise @@ -44,6 +45,7 @@ __all__ = [ "format_date", "format_ms_time", "format_ms_time_det", + "format_pct", "get_dry_run_wallet", "FtPrecise", "PeriodicCache", diff --git a/freqtrade/util/formatters.py b/freqtrade/util/formatters.py index caac5a68b..58623de66 100644 --- a/freqtrade/util/formatters.py +++ b/freqtrade/util/formatters.py @@ -1,5 +1,7 @@ from datetime import timedelta +from numpy import isnan + from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN @@ -80,3 +82,15 @@ def format_duration(td: timedelta) -> str: h, r = divmod(td.seconds, 3600) m, _ = divmod(r, 60) return f"{d}d {h:02d}:{m:02d}" + + +def format_pct(value: float | None) -> str: + """ + Format a float value as percentage string with 2 decimals + None and NaN values are formatted as "N/A" + :param value: Float value to format + :return: Formatted percentage string + """ + if value is None or isnan(value): + return "N/A" + return f"{value:.2%}" From 2750643e07a781aa98b4de090dc6fdfb2ba3fedd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 08:14:39 +0100 Subject: [PATCH 03/10] test: add tests for format_pct --- tests/util/test_formatters.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/util/test_formatters.py b/tests/util/test_formatters.py index 8872137a0..4a760961d 100644 --- a/tests/util/test_formatters.py +++ b/tests/util/test_formatters.py @@ -1,6 +1,13 @@ from datetime import timedelta -from freqtrade.util import decimals_per_coin, fmt_coin, fmt_coin2, format_duration, round_value +from freqtrade.util import ( + decimals_per_coin, + fmt_coin, + fmt_coin2, + format_duration, + format_pct, + round_value, +) def test_decimals_per_coin(): @@ -55,3 +62,13 @@ def test_format_duration(): assert format_duration(timedelta(minutes=1445)) == "1d 00:05" assert format_duration(timedelta(minutes=11445)) == "7d 22:45" assert format_duration(timedelta(minutes=101445)) == "70d 10:45" + + +def test_format_pct(): + assert format_pct(0.1234) == "12.34%" + assert format_pct(0.1) == "10.00%" + assert format_pct(0.0) == "0.00%" + assert format_pct(-0.0567) == "-5.67%" + assert format_pct(-1.5567) == "-155.67%" + assert format_pct(None) == "N/A" + assert format_pct(float("nan")) == "N/A" From 0ce9149ddde6f1ba9a64154f8377ee0dd705d87a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 08:18:17 +0100 Subject: [PATCH 04/10] feat: use helper method to format for pct --- freqtrade/rpc/rpc.py | 3 ++- freqtrade/rpc/telegram.py | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 075e6eb0e..22e457eef 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -47,6 +47,7 @@ from freqtrade.util import ( dt_ts, dt_ts_def, format_date, + format_pct, shorten_date, ) from freqtrade.wallets import PositionWallet, Wallet @@ -302,7 +303,7 @@ class RPC: fiat_total_profit_sum = nan for trade in self._rpc_trade_status(): # Format profit as a string with the right sign - profit = f"{trade['profit_ratio']:.2%}" + profit = f"{format_pct(trade['profit_ratio'])}" fiat_profit = trade.get("profit_fiat", None) if fiat_profit is None or isnan(fiat_profit): fiat_profit = trade.get("profit_abs", 0.0) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3db8724de..875fba5fe 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -48,6 +48,7 @@ from freqtrade.util import ( fmt_coin, fmt_coin2, format_date, + format_pct, round_value, ) @@ -800,19 +801,19 @@ class Telegram(RPCHandler): else "" ), ("*Unrealized Profit:* " if r["is_open"] else "*Close Profit: *") - + f"`{r['profit_ratio']:.2%}` `({r['profit_abs_r']})`", + + f"`{format_pct(r['profit_ratio'])}` `({r['profit_abs_r']})`", ] ) if r["is_open"]: if r.get("realized_profit"): lines.append( - f"*Realized Profit:* `{r['realized_profit_ratio']:.2%} " + f"*Realized Profit:* `{format_pct(r['realized_profit_ratio'])} " f"({r['realized_profit_r']})`" ) if r.get("total_profit_ratio"): lines.append( - f"*Total Profit:* `{r['total_profit_ratio']:.2%} " + f"*Total Profit:* `{format_pct(r['total_profit_ratio'])} " f"({r['total_profit_abs_r']})`" ) @@ -829,17 +830,17 @@ class Telegram(RPCHandler): # Adding initial stoploss only if it is different from stoploss lines.append( f"*Initial Stoploss:* `{r['initial_stop_loss_abs']:.8f}` " - f"`({r['initial_stop_loss_ratio']:.2%})`" + f"`({format_pct(r['initial_stop_loss_ratio'])})`" ) # Adding stoploss and stoploss percentage only if it is not None lines.append( f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` " - + (f"`({r['stop_loss_ratio']:.2%})`" if r["stop_loss_ratio"] else "") + + (f"`({format_pct(r['stop_loss_ratio'])})`" if r["stop_loss_ratio"] else "") ) lines.append( f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` " - f"`({r['stoploss_current_dist_ratio']:.2%})`" + f"`({format_pct(r['stoploss_current_dist_ratio'])})`" ) if open_orders := r.get("open_orders"): lines.append( From a4e6ac0c7f6f8e66e01104334d769905626173e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 08:18:51 +0100 Subject: [PATCH 05/10] test: update test for new status table behavior --- tests/rpc/test_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d2d666cbc..40e2b8266 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -310,7 +310,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker, time_machine) -> No ) assert "now" == result[0][2] assert "ETH/BTC" in result[0][1] - assert "nan%" == result[0][3] + assert "N/A" == result[0][3] assert isnan(fiat_profit_sum) From efabcef330a96dcb19ee8e9ff4cbde62d0b3939f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 08:23:05 +0100 Subject: [PATCH 06/10] feat: use pct formatting helper throughout telegram module --- freqtrade/rpc/telegram.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 875fba5fe..35668fc0a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -482,7 +482,7 @@ class Telegram(RPCHandler): if is_final_exit: profit_prefix = "Sub " cp_extra = ( - f"*Final Profit:* `{msg['final_profit_ratio']:.2%} " + f"*Final Profit:* `{format_pct(msg['final_profit_ratio'])} " f"({msg['cumulative_profit']:.8f} {msg['quote_currency']}{cp_fiat})`\n" ) else: @@ -498,7 +498,7 @@ class Telegram(RPCHandler): 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%}{profit_extra}`\n" + f"`{format_pct(msg['profit_ratio'])}{profit_extra}`\n" f"{cp_extra}" f"{enter_tag}" f"*Exit Reason:* `{msg['exit_reason']}`\n" @@ -678,7 +678,7 @@ class Telegram(RPCHandler): ) lines.append( f"*Average {wording} Price:* {round_value(cur_entry_average, 8)} " - f"({price_to_1st_entry:.2%} from 1st entry rate)" + f"({format_pct(price_to_1st_entry)} from 1st entry rate)" ) lines.append(f"*Order Filled:* {order['order_filled_date']}") @@ -951,7 +951,7 @@ class Telegram(RPCHandler): f"{period['date']:{val.dateformat}} ({period['trade_count']})", f"{fmt_coin(period['abs_profit'], stats['stake_currency'])}", f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}", - f"{period['rel_profit']:.2%}", + f"{format_pct(period['rel_profit'])}", ] for period in stats["data"] ], @@ -1067,7 +1067,7 @@ class Telegram(RPCHandler): markdown_msg = ( f"{closed_roi_label}\n" f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " - f"({profit_closed_ratio_mean:.2%}) " + f"({format_pct(profit_closed_ratio_mean)}) " f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"{fiat_closed_trades}" ) @@ -1080,7 +1080,7 @@ class Telegram(RPCHandler): markdown_msg += ( f"{all_roi_label}\n" f"∙ `{fmt_coin(profit_all_coin, stake_cur)} " - f"({profit_all_ratio_mean:.2%}) " + f"({format_pct(profit_all_ratio_mean)}) " f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"{fiat_all_trades}" f"*Total Trade Count:* `{trade_count}`\n" @@ -1089,7 +1089,7 @@ class Telegram(RPCHandler): f"`{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}`\n" f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\n" - f"*Winrate:* `{winrate:.2%}`\n" + f"*Winrate:* `{format_pct(winrate)}`\n" f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`" ) @@ -1097,16 +1097,16 @@ class Telegram(RPCHandler): markdown_msg += ( f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} " - f"({best_pair_profit_ratio:.2%})`\n" + f"({format_pct(best_pair_profit_ratio)})`\n" f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n" f"*Profit factor:* `{stats['profit_factor']:.2f}`\n" - f"*Max Drawdown:* `{stats['max_drawdown']:.2%} " + f"*Max Drawdown:* `{format_pct(stats['max_drawdown'])} " f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n" f" from `{stats['max_drawdown_start']} " f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n" f" to `{stats['max_drawdown_end']} " f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n" - f"*Current Drawdown:* `{stats['current_drawdown']:.2%} " + f"*Current Drawdown:* `{format_pct(stats['current_drawdown'])} " f"({fmt_coin(stats['current_drawdown_abs'], stake_cur)})`\n" f" from `{stats['current_drawdown_start']} " f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n" @@ -1559,7 +1559,7 @@ class Telegram(RPCHandler): dt_humanize_delta(dt_from_ts(trade["close_timestamp"])), f"{trade['pair']} (#{trade['trade_id']}" f"{(' ' + ('S' if trade['is_short'] else 'L')) if nonspot else ''})", - f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})", + f"{format_pct(trade['close_profit'])} ({trade['close_profit_abs']})", ] for trade in trades["trades"] ], @@ -1623,7 +1623,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i + 1}.\t {trade['pair']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit_ratio']:.2%}) " + f"({format_pct(trade['profit_ratio'])}) " f"({trade['count']})\n" ) @@ -1660,7 +1660,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i + 1}.\t `{trade['enter_tag']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit_ratio']:.2%}) " + f"({format_pct(trade['profit_ratio'])}) " f"({trade['count']})`\n" ) @@ -1697,7 +1697,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i + 1}.\t `{trade['exit_reason']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit_ratio']:.2%}) " + f"({format_pct(trade['profit_ratio'])}) " f"({trade['count']})`\n" ) @@ -1734,7 +1734,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i + 1}.\t `{trade['mix_tag']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit_ratio']:.2%}) " + f"({format_pct(trade['profit_ratio'])}) " f"({trade['count']})`\n" ) From d0fb4cdc37948ae9e52e308043e9197dacdcc847 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 08:30:10 +0100 Subject: [PATCH 07/10] feat: use N/A for round value --- freqtrade/util/formatters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/util/formatters.py b/freqtrade/util/formatters.py index 58623de66..3d7493a2a 100644 --- a/freqtrade/util/formatters.py +++ b/freqtrade/util/formatters.py @@ -31,6 +31,8 @@ def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str: :param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2" :return: Rounded value as string """ + if isnan(value): + return "N/A" val = f"{value:.{decimals}f}" if not keep_trailing_zeros: val = strip_trailing_zeros(val) From 649db69314486e3983f901d9f47222a008ce3b85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 08:30:24 +0100 Subject: [PATCH 08/10] test: add tests for N/A when formatting prices --- tests/util/test_formatters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/util/test_formatters.py b/tests/util/test_formatters.py index 4a760961d..a884c0750 100644 --- a/tests/util/test_formatters.py +++ b/tests/util/test_formatters.py @@ -32,6 +32,7 @@ def test_fmt_coin(): assert fmt_coin(0.1274512123, "BTC", False) == "0.12745121" assert fmt_coin(0.1274512123, "ETH", False) == "0.12745" assert fmt_coin(222.2, "USDT", False, True) == "222.200" + assert fmt_coin(float("nan"), "USDT", False, True) == "N/A" def test_fmt_coin2(): @@ -42,6 +43,7 @@ def test_fmt_coin2(): assert fmt_coin2(0.1274512123, "BTC") == "0.12745121 BTC" assert fmt_coin2(0.1274512123, "ETH") == "0.12745121 ETH" assert fmt_coin2(0.00001245, "PEPE") == "0.00001245 PEPE" + assert fmt_coin2(float("nan"), "PEPE") == "N/A PEPE" def test_round_value(): @@ -53,6 +55,8 @@ def test_round_value(): assert round_value(0.1274512123, 5) == "0.12745" assert round_value(222.2, 3, True) == "222.200" assert round_value(222.2, 0, True) == "222" + assert round_value(float("nan"), 0, True) == "N/A" + assert round_value(float("nan"), 10, True) == "N/A" def test_format_duration(): From 972e25a6a768a9e391e3c051c353e5a84dd0d383 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 08:33:11 +0100 Subject: [PATCH 09/10] feat: Improve conditions to not exclude `0.0` profits --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 35668fc0a..8aacb236f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -806,12 +806,12 @@ class Telegram(RPCHandler): ) if r["is_open"]: - if r.get("realized_profit"): + if r.get("realized_profit") is not None: lines.append( f"*Realized Profit:* `{format_pct(r['realized_profit_ratio'])} " f"({r['realized_profit_r']})`" ) - if r.get("total_profit_ratio"): + if r.get("total_profit_ratio") is not None: lines.append( f"*Total Profit:* `{format_pct(r['total_profit_ratio'])} " f"({r['total_profit_abs_r']})`" From 7edc2e8c940c793b3770e56ce6ad5059b3af2e76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Nov 2025 13:23:18 +0100 Subject: [PATCH 10/10] fix: improved realized profit telegram condition --- freqtrade/rpc/telegram.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8aacb236f..151d6daec 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -806,7 +806,10 @@ class Telegram(RPCHandler): ) if r["is_open"]: - if r.get("realized_profit") is not None: + if ( + r.get("realized_profit") is not None + and r.get("realized_profit_ratio") is not None + ): lines.append( f"*Realized Profit:* `{format_pct(r['realized_profit_ratio'])} " f"({r['realized_profit_r']})`"