Merge pull request #12542 from freqtrade/feat/no_data

Fix telegram crash during exchange downtime and improve percentage formatting
This commit is contained in:
Matthias
2025-11-23 15:43:07 +01:00
committed by GitHub
6 changed files with 75 additions and 32 deletions

View File

@@ -47,6 +47,7 @@ from freqtrade.util import (
dt_ts, dt_ts,
dt_ts_def, dt_ts_def,
format_date, format_date,
format_pct,
shorten_date, shorten_date,
) )
from freqtrade.wallets import PositionWallet, Wallet from freqtrade.wallets import PositionWallet, Wallet
@@ -302,7 +303,7 @@ class RPC:
fiat_total_profit_sum = nan fiat_total_profit_sum = nan
for trade in self._rpc_trade_status(): for trade in self._rpc_trade_status():
# Format profit as a string with the right sign # 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) fiat_profit = trade.get("profit_fiat", None)
if fiat_profit is None or isnan(fiat_profit): if fiat_profit is None or isnan(fiat_profit):
fiat_profit = trade.get("profit_abs", 0.0) fiat_profit = trade.get("profit_abs", 0.0)

View File

@@ -48,6 +48,7 @@ from freqtrade.util import (
fmt_coin, fmt_coin,
fmt_coin2, fmt_coin2,
format_date, format_date,
format_pct,
round_value, round_value,
) )
@@ -481,7 +482,7 @@ class Telegram(RPCHandler):
if is_final_exit: if is_final_exit:
profit_prefix = "Sub " profit_prefix = "Sub "
cp_extra = ( 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" f"({msg['cumulative_profit']:.8f} {msg['quote_currency']}{cp_fiat})`\n"
) )
else: else:
@@ -497,7 +498,7 @@ class Telegram(RPCHandler):
f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n" f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n"
f"{self._add_analyzed_candle(msg['pair'])}" f"{self._add_analyzed_candle(msg['pair'])}"
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " 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"{cp_extra}"
f"{enter_tag}" f"{enter_tag}"
f"*Exit Reason:* `{msg['exit_reason']}`\n" f"*Exit Reason:* `{msg['exit_reason']}`\n"
@@ -677,7 +678,7 @@ class Telegram(RPCHandler):
) )
lines.append( lines.append(
f"*Average {wording} Price:* {round_value(cur_entry_average, 8)} " 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']}") lines.append(f"*Order Filled:* {order['order_filled_date']}")
@@ -800,21 +801,23 @@ class Telegram(RPCHandler):
else "" else ""
), ),
("*Unrealized Profit:* " if r["is_open"] else "*Close Profit: *") ("*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["is_open"]:
if r.get("realized_profit"): if (
lines.extend( r.get("realized_profit") is not None
[ and r.get("realized_profit_ratio") is not None
f"*Realized Profit:* `{r['realized_profit_ratio']:.2%} " ):
f"({r['realized_profit_r']})`", lines.append(
( f"*Realized Profit:* `{format_pct(r['realized_profit_ratio'])} "
f"*Total Profit:* `{r['total_profit_ratio']:.2%} " f"({r['realized_profit_r']})`"
f"({r['total_profit_abs_r']})`" )
), 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']})`"
) )
# Append empty line to improve readability # Append empty line to improve readability
@@ -830,17 +833,17 @@ class Telegram(RPCHandler):
# Adding initial stoploss only if it is different from stoploss # Adding initial stoploss only if it is different from stoploss
lines.append( lines.append(
f"*Initial Stoploss:* `{r['initial_stop_loss_abs']:.8f}` " 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 # Adding stoploss and stoploss percentage only if it is not None
lines.append( lines.append(
f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` " 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( lines.append(
f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` " 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"): if open_orders := r.get("open_orders"):
lines.append( lines.append(
@@ -951,7 +954,7 @@ class Telegram(RPCHandler):
f"{period['date']:{val.dateformat}} ({period['trade_count']})", f"{period['date']:{val.dateformat}} ({period['trade_count']})",
f"{fmt_coin(period['abs_profit'], stats['stake_currency'])}", f"{fmt_coin(period['abs_profit'], stats['stake_currency'])}",
f"{period['fiat_value']:.2f} {stats['fiat_display_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"] for period in stats["data"]
], ],
@@ -1067,7 +1070,7 @@ class Telegram(RPCHandler):
markdown_msg = ( markdown_msg = (
f"{closed_roi_label}\n" f"{closed_roi_label}\n"
f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " 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"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_closed_trades}" f"{fiat_closed_trades}"
) )
@@ -1080,7 +1083,7 @@ class Telegram(RPCHandler):
markdown_msg += ( markdown_msg += (
f"{all_roi_label}\n" f"{all_roi_label}\n"
f"∙ `{fmt_coin(profit_all_coin, stake_cur)} " 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"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_all_trades}" f"{fiat_all_trades}"
f"*Total Trade Count:* `{trade_count}`\n" f"*Total Trade Count:* `{trade_count}`\n"
@@ -1089,7 +1092,7 @@ class Telegram(RPCHandler):
f"`{first_trade_date}`\n" f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}`\n"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\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})`" f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`"
) )
@@ -1097,16 +1100,16 @@ class Telegram(RPCHandler):
markdown_msg += ( markdown_msg += (
f"\n*Avg. Duration:* `{avg_duration}`\n" f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} " 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"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\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"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['max_drawdown_start']} " f" from `{stats['max_drawdown_start']} "
f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n" f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n"
f" to `{stats['max_drawdown_end']} " f" to `{stats['max_drawdown_end']} "
f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n" 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"({fmt_coin(stats['current_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['current_drawdown_start']} " f" from `{stats['current_drawdown_start']} "
f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n" f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n"
@@ -1559,7 +1562,7 @@ class Telegram(RPCHandler):
dt_humanize_delta(dt_from_ts(trade["close_timestamp"])), dt_humanize_delta(dt_from_ts(trade["close_timestamp"])),
f"{trade['pair']} (#{trade['trade_id']}" f"{trade['pair']} (#{trade['trade_id']}"
f"{(' ' + ('S' if trade['is_short'] else 'L')) if nonspot else ''})", 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"] for trade in trades["trades"]
], ],
@@ -1623,7 +1626,7 @@ class Telegram(RPCHandler):
stat_line = ( stat_line = (
f"{i + 1}.\t <code>{trade['pair']}\t" f"{i + 1}.\t <code>{trade['pair']}\t"
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit_ratio']:.2%}) " f"({format_pct(trade['profit_ratio'])}) "
f"({trade['count']})</code>\n" f"({trade['count']})</code>\n"
) )
@@ -1660,7 +1663,7 @@ class Telegram(RPCHandler):
stat_line = ( stat_line = (
f"{i + 1}.\t `{trade['enter_tag']}\t" f"{i + 1}.\t `{trade['enter_tag']}\t"
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " 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" f"({trade['count']})`\n"
) )
@@ -1697,7 +1700,7 @@ class Telegram(RPCHandler):
stat_line = ( stat_line = (
f"{i + 1}.\t `{trade['exit_reason']}\t" f"{i + 1}.\t `{trade['exit_reason']}\t"
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " 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" f"({trade['count']})`\n"
) )
@@ -1734,7 +1737,7 @@ class Telegram(RPCHandler):
stat_line = ( stat_line = (
f"{i + 1}.\t `{trade['mix_tag']}\t" f"{i + 1}.\t `{trade['mix_tag']}\t"
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " 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" f"({trade['count']})`\n"
) )

View File

@@ -18,6 +18,7 @@ from freqtrade.util.formatters import (
fmt_coin, fmt_coin,
fmt_coin2, fmt_coin2,
format_duration, format_duration,
format_pct,
round_value, round_value,
) )
from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.ft_precise import FtPrecise
@@ -44,6 +45,7 @@ __all__ = [
"format_date", "format_date",
"format_ms_time", "format_ms_time",
"format_ms_time_det", "format_ms_time_det",
"format_pct",
"get_dry_run_wallet", "get_dry_run_wallet",
"FtPrecise", "FtPrecise",
"PeriodicCache", "PeriodicCache",

View File

@@ -1,5 +1,7 @@
from datetime import timedelta from datetime import timedelta
from numpy import isnan
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
@@ -29,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" :param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
:return: Rounded value as string :return: Rounded value as string
""" """
if isnan(value):
return "N/A"
val = f"{value:.{decimals}f}" val = f"{value:.{decimals}f}"
if not keep_trailing_zeros: if not keep_trailing_zeros:
val = strip_trailing_zeros(val) val = strip_trailing_zeros(val)
@@ -80,3 +84,15 @@ def format_duration(td: timedelta) -> str:
h, r = divmod(td.seconds, 3600) h, r = divmod(td.seconds, 3600)
m, _ = divmod(r, 60) m, _ = divmod(r, 60)
return f"{d}d {h:02d}:{m:02d}" 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%}"

View File

@@ -310,7 +310,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker, time_machine) -> No
) )
assert "now" == result[0][2] assert "now" == result[0][2]
assert "ETH/BTC" in result[0][1] assert "ETH/BTC" in result[0][1]
assert "nan%" == result[0][3] assert "N/A" == result[0][3]
assert isnan(fiat_profit_sum) assert isnan(fiat_profit_sum)

View File

@@ -1,6 +1,13 @@
from datetime import timedelta 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(): def test_decimals_per_coin():
@@ -25,6 +32,7 @@ def test_fmt_coin():
assert fmt_coin(0.1274512123, "BTC", False) == "0.12745121" assert fmt_coin(0.1274512123, "BTC", False) == "0.12745121"
assert fmt_coin(0.1274512123, "ETH", False) == "0.12745" assert fmt_coin(0.1274512123, "ETH", False) == "0.12745"
assert fmt_coin(222.2, "USDT", False, True) == "222.200" assert fmt_coin(222.2, "USDT", False, True) == "222.200"
assert fmt_coin(float("nan"), "USDT", False, True) == "N/A"
def test_fmt_coin2(): def test_fmt_coin2():
@@ -35,6 +43,7 @@ def test_fmt_coin2():
assert fmt_coin2(0.1274512123, "BTC") == "0.12745121 BTC" assert fmt_coin2(0.1274512123, "BTC") == "0.12745121 BTC"
assert fmt_coin2(0.1274512123, "ETH") == "0.12745121 ETH" assert fmt_coin2(0.1274512123, "ETH") == "0.12745121 ETH"
assert fmt_coin2(0.00001245, "PEPE") == "0.00001245 PEPE" assert fmt_coin2(0.00001245, "PEPE") == "0.00001245 PEPE"
assert fmt_coin2(float("nan"), "PEPE") == "N/A PEPE"
def test_round_value(): def test_round_value():
@@ -46,6 +55,8 @@ def test_round_value():
assert round_value(0.1274512123, 5) == "0.12745" assert round_value(0.1274512123, 5) == "0.12745"
assert round_value(222.2, 3, True) == "222.200" assert round_value(222.2, 3, True) == "222.200"
assert round_value(222.2, 0, True) == "222" 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(): def test_format_duration():
@@ -55,3 +66,13 @@ def test_format_duration():
assert format_duration(timedelta(minutes=1445)) == "1d 00:05" assert format_duration(timedelta(minutes=1445)) == "1d 00:05"
assert format_duration(timedelta(minutes=11445)) == "7d 22:45" assert format_duration(timedelta(minutes=11445)) == "7d 22:45"
assert format_duration(timedelta(minutes=101445)) == "70d 10: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"