Skip to content

Commit 64c6b47

Browse files
authored
Merge pull request freqtrade#11758 from mrpabloyeah/add-max-trade-duration-to-backtest-results
Add min/max trade duration to backtest results
2 parents d1ebdf5 + 947cbdd commit 64c6b47

File tree

6 files changed

+86
-15
lines changed

6 files changed

+86
-15
lines changed

freqtrade/optimize/optimize_reports/bt_output.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,18 @@ def text_table_add_metrics(strat_results: dict) -> None:
370370
f"{strat_results['winning_days']} / "
371371
f"{strat_results['draw_days']} / {strat_results['losing_days']}",
372372
),
373-
("Avg. Duration Winners", f"{strat_results['winner_holding_avg']}"),
374-
("Avg. Duration Loser", f"{strat_results['loser_holding_avg']}"),
373+
(
374+
"Min/Max/Avg. Duration Winners",
375+
f"{strat_results.get('winner_holding_min', 'N/A')} / "
376+
f"{strat_results.get('winner_holding_max', 'N/A')} / "
377+
f"{strat_results.get('winner_holding_avg', 'N/A')}",
378+
),
379+
(
380+
"Min/Max/Avg. Duration Losers",
381+
f"{strat_results.get('loser_holding_min', 'N/A')} / "
382+
f"{strat_results.get('loser_holding_max', 'N/A')} / "
383+
f"{strat_results.get('loser_holding_avg', 'N/A')}",
384+
),
375385
(
376386
"Max Consecutive Wins / Loss",
377387
(

freqtrade/optimize/optimize_reports/optimize_reports.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
BacktestResultType,
2424
get_BacktestResultType_default,
2525
)
26-
from freqtrade.util import decimals_per_coin, fmt_coin, get_dry_run_wallet
26+
from freqtrade.util import decimals_per_coin, fmt_coin, format_duration, get_dry_run_wallet
2727

2828

2929
logger = logging.getLogger(__name__)
@@ -336,22 +336,44 @@ def generate_trading_stats(results: DataFrame) -> dict[str, Any]:
336336
}
337337

338338
winning_trades = results.loc[results["profit_ratio"] > 0]
339+
winning_duration = winning_trades["trade_duration"]
339340
draw_trades = results.loc[results["profit_ratio"] == 0]
340341
losing_trades = results.loc[results["profit_ratio"] < 0]
342+
losing_duration = losing_trades["trade_duration"]
341343

342344
holding_avg = (
343345
timedelta(minutes=round(results["trade_duration"].mean()))
344346
if not results.empty
345347
else timedelta()
346348
)
349+
winner_holding_min = (
350+
timedelta(minutes=round(winning_duration[winning_duration > 0].min()))
351+
if not winning_duration.empty
352+
else timedelta()
353+
)
354+
winner_holding_max = (
355+
timedelta(minutes=round(winning_duration.max()))
356+
if not winning_duration.empty
357+
else timedelta()
358+
)
347359
winner_holding_avg = (
348-
timedelta(minutes=round(winning_trades["trade_duration"].mean()))
349-
if not winning_trades.empty
360+
timedelta(minutes=round(winning_duration.mean()))
361+
if not winning_duration.empty
362+
else timedelta()
363+
)
364+
loser_holding_min = (
365+
timedelta(minutes=round(losing_duration[losing_duration > 0].min()))
366+
if not losing_duration.empty
367+
else timedelta()
368+
)
369+
loser_holding_max = (
370+
timedelta(minutes=round(losing_duration.max()))
371+
if not losing_duration.empty
350372
else timedelta()
351373
)
352374
loser_holding_avg = (
353-
timedelta(minutes=round(losing_trades["trade_duration"].mean()))
354-
if not losing_trades.empty
375+
timedelta(minutes=round(losing_duration.mean()))
376+
if not losing_duration.empty
355377
else timedelta()
356378
)
357379
winstreak, loss_streak = calc_streak(results)
@@ -363,9 +385,17 @@ def generate_trading_stats(results: DataFrame) -> dict[str, Any]:
363385
"winrate": len(winning_trades) / len(results) if len(results) else 0.0,
364386
"holding_avg": holding_avg,
365387
"holding_avg_s": holding_avg.total_seconds(),
366-
"winner_holding_avg": winner_holding_avg,
388+
"winner_holding_min": format_duration(winner_holding_min),
389+
"winner_holding_min_s": winner_holding_min.total_seconds(),
390+
"winner_holding_max": format_duration(winner_holding_max),
391+
"winner_holding_max_s": winner_holding_max.total_seconds(),
392+
"winner_holding_avg": format_duration(winner_holding_avg),
367393
"winner_holding_avg_s": winner_holding_avg.total_seconds(),
368-
"loser_holding_avg": loser_holding_avg,
394+
"loser_holding_min": format_duration(loser_holding_min),
395+
"loser_holding_min_s": loser_holding_min.total_seconds(),
396+
"loser_holding_max": format_duration(loser_holding_max),
397+
"loser_holding_max_s": loser_holding_max.total_seconds(),
398+
"loser_holding_avg": format_duration(loser_holding_avg),
369399
"loser_holding_avg_s": loser_holding_avg.total_seconds(),
370400
"max_consecutive_wins": winstreak,
371401
"max_consecutive_losses": loss_streak,

freqtrade/util/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@
1313
shorten_date,
1414
)
1515
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
16-
from freqtrade.util.formatters import decimals_per_coin, fmt_coin, fmt_coin2, round_value
16+
from freqtrade.util.formatters import (
17+
decimals_per_coin,
18+
fmt_coin,
19+
fmt_coin2,
20+
format_duration,
21+
round_value,
22+
)
1723
from freqtrade.util.ft_precise import FtPrecise
1824
from freqtrade.util.measure_time import MeasureTime
1925
from freqtrade.util.periodic_cache import PeriodicCache
@@ -44,6 +50,7 @@
4450
"shorten_date",
4551
"decimals_per_coin",
4652
"round_value",
53+
"format_duration",
4754
"fmt_coin",
4855
"fmt_coin2",
4956
"MeasureTime",

freqtrade/util/formatters.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import timedelta
2+
13
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
24

35

@@ -66,3 +68,15 @@ def fmt_coin2(
6668
val = f"{val} {coin}"
6769

6870
return val
71+
72+
73+
def format_duration(td: timedelta) -> str:
74+
"""
75+
Format a timedelta object to "XXd HH:MM" format
76+
:param td: Timedelta object to format
77+
:return: Formatted time string
78+
"""
79+
d = td.days
80+
h, r = divmod(td.seconds, 3600)
81+
m, s = divmod(r, 60)
82+
return f"{d}d {h:02d}:{m:02d}"

tests/optimize/test_optimize_reports.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
generate_tag_metrics,
4141
)
4242
from freqtrade.resolvers.strategy_resolver import StrategyResolver
43-
from freqtrade.util import dt_ts
43+
from freqtrade.util import dt_ts, format_duration
4444
from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc
4545
from tests.conftest import CURRENT_TEST_STRATEGY, log_has_re
4646
from tests.data.test_history import _clean_test_file
@@ -482,8 +482,8 @@ def test_generate_trading_stats(testdatadir):
482482
bt_data = load_backtest_data(filename)
483483
res = generate_trading_stats(bt_data)
484484
assert isinstance(res, dict)
485-
assert res["winner_holding_avg"] == timedelta(seconds=1440)
486-
assert res["loser_holding_avg"] == timedelta(days=1, seconds=21420)
485+
assert res["winner_holding_avg"] == format_duration(timedelta(seconds=1440))
486+
assert res["loser_holding_avg"] == format_duration(timedelta(days=1, seconds=21420))
487487
assert "wins" in res
488488
assert "losses" in res
489489
assert "draws" in res

tests/util/test_formatters.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from freqtrade.util import decimals_per_coin, fmt_coin, round_value
2-
from freqtrade.util.formatters import fmt_coin2
1+
from datetime import timedelta
2+
3+
from freqtrade.util import decimals_per_coin, fmt_coin, fmt_coin2, format_duration, round_value
34

45

56
def test_decimals_per_coin():
@@ -45,3 +46,12 @@ def test_round_value():
4546
assert round_value(0.1274512123, 5) == "0.12745"
4647
assert round_value(222.2, 3, True) == "222.200"
4748
assert round_value(222.2, 0, True) == "222"
49+
50+
51+
def test_format_duration():
52+
assert format_duration(timedelta(minutes=5)) == "0d 00:05"
53+
assert format_duration(timedelta(minutes=75)) == "0d 01:15"
54+
assert format_duration(timedelta(minutes=1440)) == "1d 00:00"
55+
assert format_duration(timedelta(minutes=1445)) == "1d 00:05"
56+
assert format_duration(timedelta(minutes=11445)) == "7d 22:45"
57+
assert format_duration(timedelta(minutes=101445)) == "70d 10:45"

0 commit comments

Comments
 (0)