Merge pull request #12389 from mrpabloyeah/add-day-of-week-to-backtest-breakdowns

Add day of week to backtest breakdowns
This commit is contained in:
Matthias
2025-10-16 20:16:02 +02:00
committed by GitHub
8 changed files with 82 additions and 36 deletions

View File

@@ -268,7 +268,8 @@
"day",
"week",
"month",
"year"
"year",
"weekday"
]
}
},

View File

@@ -4,7 +4,7 @@ usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--backtest-filename PATH]
[--backtest-directory PATH]
[--show-pair-list]
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
[--breakdown {day,week,month,year,weekday} [{day,week,month,year,weekday} ...]]
options:
-h, --help show this help message and exit
@@ -18,9 +18,9 @@ options:
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--show-pair-list Show backtesting pairlist sorted by profit.
--breakdown {day,week,month,year} [{day,week,month,year} ...]
--breakdown {day,week,month,year,weekday} [{day,week,month,year,weekday} ...]
Show backtesting breakdown per [day, week, month,
year].
year, weekday].
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@@ -17,7 +17,7 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--export {none,trades,signals}]
[--backtest-filename PATH]
[--backtest-directory PATH]
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
[--breakdown {day,week,month,year,weekday} [{day,week,month,year,weekday} ...]]
[--cache {none,day,week,month}]
[--freqai-backtest-live-models] [--notes TEXT]
@@ -77,9 +77,9 @@ options:
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--breakdown {day,week,month,year} [{day,week,month,year} ...]
--breakdown {day,week,month,year,weekday} [{day,week,month,year,weekday} ...]
Show backtesting breakdown per [day, week, month,
year].
year, weekday].
--cache {none,day,week,month}
Load a cached backtest result no older than specified
age (default: day).

View File

@@ -4,7 +4,7 @@ usage: freqtrade hyperopt-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--profitable] [-n INT] [--print-json]
[--hyperopt-filename FILENAME] [--no-header]
[--disable-param-export]
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
[--breakdown {day,week,month,year,weekday} [{day,week,month,year,weekday} ...]]
options:
-h, --help show this help message and exit
@@ -18,9 +18,9 @@ options:
--no-header Do not print epoch details header.
--disable-param-export
Disable automatic hyperopt parameter export.
--breakdown {day,week,month,year} [{day,week,month,year} ...]
--breakdown {day,week,month,year,weekday} [{day,week,month,year,weekday} ...]
Show backtesting breakdown per [day, week, month,
year].
year, weekday].
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@@ -245,7 +245,7 @@ AVAILABLE_CLI_OPTIONS = {
),
"backtest_breakdown": Arg(
"--breakdown",
help="Show backtesting breakdown per [day, week, month, year].",
help="Show backtesting breakdown per [day, week, month, year, weekday].",
nargs="+",
choices=constants.BACKTEST_BREAKDOWNS,
),

View File

@@ -61,7 +61,7 @@ AVAILABLE_PAIRLISTS = [
"VolatilityFilter",
]
AVAILABLE_DATAHANDLERS = ["json", "jsongz", "feather", "parquet"]
BACKTEST_BREAKDOWNS = ["day", "week", "month", "year"]
BACKTEST_BREAKDOWNS = ["day", "week", "month", "year", "weekday"]
BACKTEST_CACHE_AGE = ["none", "day", "week", "month"]
BACKTEST_CACHE_DEFAULT = "day"
DRY_RUN_WALLET = 1000

View File

@@ -256,40 +256,66 @@ def _get_resample_from_period(period: str) -> str:
return "1ME"
if period == "year":
return "1YE"
if period == "weekday":
# Required to pass the test
return "weekday"
raise ValueError(f"Period {period} is not supported.")
def _calculate_stats_for_period(data: DataFrame) -> dict[str, Any]:
profit_abs = data["profit_abs"].sum().round(10)
wins = sum(data["profit_abs"] > 0)
draws = sum(data["profit_abs"] == 0)
losses = sum(data["profit_abs"] < 0)
trades = wins + draws + losses
winning_profit = data.loc[data["profit_abs"] > 0, "profit_abs"].sum()
losing_profit = data.loc[data["profit_abs"] < 0, "profit_abs"].sum()
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
return {
"profit_abs": profit_abs,
"wins": wins,
"draws": draws,
"losses": losses,
"trades": trades,
"profit_factor": round(profit_factor, 8),
}
def generate_periodic_breakdown_stats(
trade_list: list | DataFrame, period: str
) -> list[dict[str, Any]]:
results = trade_list if not isinstance(trade_list, list) else DataFrame.from_records(trade_list)
if len(results) == 0:
return []
results["close_date"] = to_datetime(results["close_date"], utc=True)
resample_period = _get_resample_from_period(period)
resampled = results.resample(resample_period, on="close_date")
stats = []
for name, day in resampled:
profit_abs = day["profit_abs"].sum().round(10)
wins = sum(day["profit_abs"] > 0)
draws = sum(day["profit_abs"] == 0)
losses = sum(day["profit_abs"] < 0)
trades = wins + draws + losses
winning_profit = day.loc[day["profit_abs"] > 0, "profit_abs"].sum()
losing_profit = day.loc[day["profit_abs"] < 0, "profit_abs"].sum()
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
stats.append(
{
"date": name.strftime("%d/%m/%Y"),
"date_ts": int(name.to_pydatetime().timestamp() * 1000),
"profit_abs": profit_abs,
"wins": wins,
"draws": draws,
"losses": losses,
"trades": trades,
"profit_factor": round(profit_factor, 8),
}
)
if period == "weekday":
day_names = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
results["weekday"] = results["close_date"].dt.dayofweek
stats = []
for day_num in range(7):
day_data = results[results["weekday"] == day_num]
if len(day_data) > 0:
period_stats = _calculate_stats_for_period(day_data)
stats.append({"date": day_names[day_num], "date_ts": day_num, **period_stats})
else:
resample_period = _get_resample_from_period(period)
resampled = results.resample(resample_period, on="close_date")
stats = []
for name, period_data in resampled:
period_stats = _calculate_stats_for_period(period_data)
stats.append(
{
"date": name.strftime("%d/%m/%Y"),
"date_ts": int(name.to_pydatetime().timestamp() * 1000),
**period_stats,
}
)
return stats

View File

@@ -634,11 +634,30 @@ def test_generate_periodic_breakdown_stats(testdatadir):
res = generate_periodic_breakdown_stats([], "day")
assert res == []
# Test weekday
reswd = generate_periodic_breakdown_stats(bt_data, "weekday")
assert isinstance(reswd, list)
assert len(reswd) == 7
assert reswd[0]["date"] == "Monday"
assert reswd[0]["date_ts"] == 0
assert reswd[1]["date"] == "Tuesday"
assert reswd[2]["date"] == "Wednesday"
assert reswd[3]["date"] == "Thursday"
assert reswd[4]["date"] == "Friday"
assert reswd[5]["date"] == "Saturday"
assert reswd[6]["date"] == "Sunday"
monday = reswd[0]
assert "draws" in monday
assert "losses" in monday
assert "wins" in monday
assert "profit_abs" in monday
def test__get_resample_from_period():
assert _get_resample_from_period("day") == "1d"
assert _get_resample_from_period("week") == "1W-MON"
assert _get_resample_from_period("month") == "1ME"
assert _get_resample_from_period("weekday") == "weekday"
with pytest.raises(ValueError, match=r"Period noooo is not supported."):
_get_resample_from_period("noooo")