Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions freqtrade/data/history/history_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,10 @@ def refresh_backtest_ohlcv_data(
for timeframe in timeframes:
# Get fast candles via parallel method on first loop through per timeframe
# and candle type. Downloads all the pairs in the list and stores them.
# Also skips if only 1 pair/timeframe combination is scheduled for download.
if (
not no_parallel_download
and (len(pairs) + len(timeframes)) > 2
and exchange.get_option("download_data_parallel_quick", True)
and (
((pair, timeframe, candle_type) not in fast_candles)
Expand Down Expand Up @@ -474,18 +476,20 @@ def _download_all_pairs_history_parallel(
:return: Candle pairs with timeframes
"""
candles: dict[PairWithTimeframe, DataFrame] = {}
since = 0
since: int | None = None
if timerange:
if timerange.starttype == "date":
since = timerange.startts * 1000

candle_limit = exchange.ohlcv_candle_limit(timeframe, candle_type)
one_call_min_time_dt = dt_ts(date_minus_candles(timeframe, candle_limit))
# check if we can get all candles in one go, if so then we can download them in parallel
if since > one_call_min_time_dt:
if since is None or since > one_call_min_time_dt:
logger.info(
f"Downloading parallel candles for {timeframe} for all pairs "
f"since {format_ms_time(since)}"
f"Downloading parallel candles for {timeframe} for all pairs"
f" since {format_ms_time(since)}"
if since
else "."
)
needed_pairs: ListPairsWithTimeframes = [
(p, timeframe, candle_type) for p in [p for p in pairs]
Expand Down
3 changes: 2 additions & 1 deletion freqtrade/rpc/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
dt_ts,
dt_ts_def,
format_date,
format_pct,
shorten_date,
)
from freqtrade.wallets import PositionWallet, Wallet
Expand Down Expand Up @@ -302,7 +303,7 @@ def _rpc_status_table(
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)
Expand Down
61 changes: 32 additions & 29 deletions freqtrade/rpc/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
fmt_coin,
fmt_coin2,
format_date,
format_pct,
round_value,
)

Expand Down Expand Up @@ -481,7 +482,7 @@ def _format_exit_msg(self, msg: RPCExitMsg) -> str:
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:
Expand All @@ -497,7 +498,7 @@ def _format_exit_msg(self, msg: RPCExitMsg) -> str:
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"
Expand Down Expand Up @@ -677,7 +678,7 @@ def _prepare_order_details(self, filled_orders: list, quote_currency: str, is_op
)
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']}")

Expand Down Expand Up @@ -800,21 +801,23 @@ async def _status_msg(self, update: Update, context: CallbackContext) -> None:
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.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']})`"
),
]
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']})`"
)
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
Expand All @@ -830,17 +833,17 @@ async def _status_msg(self, update: Update, context: CallbackContext) -> None:
# 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(
Expand Down Expand Up @@ -951,7 +954,7 @@ async def _timeunit_stats(self, update: Update, context: CallbackContext, unit:
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"]
],
Expand Down Expand Up @@ -1067,7 +1070,7 @@ def _format_profit_message(
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}"
)
Expand All @@ -1080,7 +1083,7 @@ def _format_profit_message(
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"
Expand All @@ -1089,24 +1092,24 @@ def _format_profit_message(
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})`"
)

if stats["closed_trade_count"] > 0:
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"
Expand Down Expand Up @@ -1559,7 +1562,7 @@ async def _trades(self, update: Update, context: CallbackContext) -> None:
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"]
],
Expand Down Expand Up @@ -1623,7 +1626,7 @@ async def _performance(self, update: Update, context: CallbackContext) -> None:
stat_line = (
f"{i + 1}.\t <code>{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']})</code>\n"
)

Expand Down Expand Up @@ -1660,7 +1663,7 @@ async def _enter_tag_performance(self, update: Update, context: CallbackContext)
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"
)

Expand Down Expand Up @@ -1697,7 +1700,7 @@ async def _exit_reason_performance(self, update: Update, context: CallbackContex
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"
)

Expand Down Expand Up @@ -1734,7 +1737,7 @@ async def _mix_tag_performance(self, update: Update, context: CallbackContext) -
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"
)

Expand Down
2 changes: 2 additions & 0 deletions freqtrade/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
fmt_coin,
fmt_coin2,
format_duration,
format_pct,
round_value,
)
from freqtrade.util.ft_precise import FtPrecise
Expand All @@ -44,6 +45,7 @@
"format_date",
"format_ms_time",
"format_ms_time_det",
"format_pct",
"get_dry_run_wallet",
"FtPrecise",
"PeriodicCache",
Expand Down
16 changes: 16 additions & 0 deletions freqtrade/util/formatters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from datetime import timedelta

from numpy import isnan

from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN


Expand Down Expand Up @@ -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"
: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)
Expand Down Expand Up @@ -80,3 +84,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%}"
22 changes: 20 additions & 2 deletions tests/data/test_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,23 @@ def parallel_mock(pairs, timeframe, candle_type, **kwargs):
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 8h\.", caplog)
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 4h\.", caplog)

# Test with only one pair - no parallel download should happen 1 pair/timeframe combination
# doesn't justify parallelization
parallel_mock.reset_mock()
dl_mock.reset_mock()
refresh_backtest_ohlcv_data(
exchange=ex,
pairs=[
"ETH/BTC",
],
timeframes=["5m"],
datadir=testdatadir,
timerange=timerange,
erase=False,
trading_mode=trademode,
)
assert parallel_mock.call_count == 0


def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
dl_mock = mocker.patch(
Expand Down Expand Up @@ -780,15 +797,16 @@ def test_download_all_pairs_history_parallel(mocker, default_conf_usdt):
exchange.refresh_latest_ohlcv.reset_mock()

# Test without timerange
# expected to call refresh_latest_ohlcv - as we can't know how much will be required.
result3 = _download_all_pairs_history_parallel(
exchange=exchange,
pairs=pairs,
timeframe=timeframe,
candle_type=candle_type,
timerange=None,
)
assert result3 == {}
assert exchange.refresh_latest_ohlcv.call_count == 0
assert result3 == expected
assert exchange.refresh_latest_ohlcv.call_count == 1


def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path, caplog) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/rpc/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
23 changes: 22 additions & 1 deletion tests/util/test_formatters.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -25,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():
Expand All @@ -35,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():
Expand All @@ -46,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():
Expand All @@ -55,3 +66,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"
Loading