From 6ddbc8c00d60f77c18256e1f72406e72c5fb2985 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Jul 2023 19:57:47 +0200 Subject: [PATCH 1/7] Move generate_wins_draw_losses to bt_output (it's an output function, not a calculation) --- freqtrade/optimize/optimize_reports/__init__.py | 4 ++-- freqtrade/optimize/optimize_reports/bt_output.py | 13 +++++++++++-- .../optimize/optimize_reports/optimize_reports.py | 10 ---------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/__init__.py b/freqtrade/optimize/optimize_reports/__init__.py index 68e222d00..9e3ac46bc 100644 --- a/freqtrade/optimize/optimize_reports/__init__.py +++ b/freqtrade/optimize/optimize_reports/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 from freqtrade.optimize.optimize_reports.bt_output import (generate_edge_table, + generate_wins_draws_losses, show_backtest_result, show_backtest_results, show_sorted_pairlist, @@ -14,5 +15,4 @@ from freqtrade.optimize.optimize_reports.optimize_reports import ( generate_all_periodic_breakdown_stats, generate_backtest_stats, generate_daily_stats, generate_exit_reason_stats, generate_pair_metrics, generate_periodic_breakdown_stats, generate_rejected_signals, generate_strategy_comparison, generate_strategy_stats, - generate_tag_metrics, generate_trade_signal_candles, generate_trading_stats, - generate_wins_draws_losses) + generate_tag_metrics, generate_trade_signal_candles, generate_trading_stats) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 07f23a5fa..d33ae4070 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -5,8 +5,7 @@ from tabulate import tabulate from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.misc import decimals_per_coin, round_coin_value -from freqtrade.optimize.optimize_reports.optimize_reports import (generate_periodic_breakdown_stats, - generate_wins_draws_losses) +from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats logger = logging.getLogger(__name__) @@ -30,6 +29,16 @@ def _get_line_header(first_column: str, stake_currency: str, 'Win Draw Loss Win%'] +def generate_wins_draws_losses(wins, draws, losses): + if wins > 0 and losses == 0: + wl_ratio = '100' + elif wins == 0: + wl_ratio = '0' + else: + wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100' + return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}' + + def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 729f61cc8..91c2fa750 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -57,16 +57,6 @@ def generate_rejected_signals(preprocessed_df: Dict[str, DataFrame], return rejected_candles_only -def generate_wins_draws_losses(wins, draws, losses): - if wins > 0 and losses == 0: - wl_ratio = '100' - elif wins == 0: - wl_ratio = '0' - else: - wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100' - return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}' - - def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: """ Generate one result dict, with "first_column" as key. From a7bd6725f5d6453988566ad0fa4abe0e36b0b370 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jul 2023 06:36:16 +0200 Subject: [PATCH 2/7] Add test to verify consecutive wins / losses calculation --- tests/optimize/test_optimize_reports.py | 26 ++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 7b85e7978..4ff8a981d 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -23,7 +23,8 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera store_backtest_analysis_results, store_backtest_stats, text_table_bt_results, text_table_exit_reason, text_table_strategy) -from freqtrade.optimize.optimize_reports.optimize_reports import _get_resample_from_period +from freqtrade.optimize.optimize_reports.optimize_reports import (_get_resample_from_period, + calc_consecutive) from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.util import dt_ts from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc @@ -348,6 +349,29 @@ def test_generate_trading_stats(testdatadir): assert res['losses'] == 0 +def test_calculate_consecutive(testdatadir): + df = pd.DataFrame({ + 'profit_ratio': [0.05, -0.02, -0.03, -0.05, 0.01, 0.02, 0.03, 0.04, -0.02, -0.03], + }) + # 4 consecutive wins, 3 consecutive losses + assert calc_consecutive(df) == (4, 3) + + # invert situation + df1 = df.copy() + df1['profit_ratio'] = df1['profit_ratio'] * -1 + assert calc_consecutive(df1) == (3, 4) + + df_empty = pd.DataFrame({ + 'profit_ratio': [], + }) + assert df_empty.empty + assert calc_consecutive(df_empty) == (0, 0) + + filename = testdatadir / "backtest_results/backtest-result.json" + bt_data = load_backtest_data(filename) + assert calc_consecutive(bt_data) == (7, 18) + + def test_text_table_exit_reason(): results = pd.DataFrame( From 0f046ceaf22d4acb21d3a4a604ea2576965ec26a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jul 2023 06:36:24 +0200 Subject: [PATCH 3/7] Implement calc_consecutive_losses --- .../optimize_reports/optimize_reports.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 91c2fa750..afcdf531c 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -1,9 +1,10 @@ import logging from copy import deepcopy from datetime import datetime, timedelta, timezone -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Tuple, Union -from pandas import DataFrame, concat, to_datetime +import numpy as np +from pandas import DataFrame, Series, concat, to_datetime from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, IntOrInf from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, @@ -252,6 +253,23 @@ def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]: return result +def calc_consecutive(dataframe: DataFrame) -> Tuple[int, int]: + """ + Calculate consecutive wins and losses + :param dataframe: Dataframe containing the trades dataframe, with profit_ratio column + :return: Tuple containing consecutive wins and losses + """ + + df = Series(np.where(dataframe['profit_ratio'] > 0, 'win', 'loss')).to_frame('result') + df['streaks'] = df['result'].ne(df['result'].shift()).cumsum().rename('streaks') + df['counter'] = df['streaks'].groupby(df['streaks']).cumcount() + 1 + res = df.groupby(df['result']).max() + # + cons_wins = res.loc['win', 'counter'] if 'win' in res.index else 0 + cons_losses = res.loc['loss', 'counter'] if 'loss' in res.index else 0 + return cons_wins, cons_losses + + def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: """ Generate overall trade statistics """ if len(results) == 0: @@ -263,6 +281,8 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: 'holding_avg': timedelta(), 'winner_holding_avg': timedelta(), 'loser_holding_avg': timedelta(), + 'max_consecutive_wins': 0, + 'max_consecutive_losses': 0, } winning_trades = results.loc[results['profit_ratio'] > 0] @@ -275,6 +295,7 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: if not winning_trades.empty else timedelta()) loser_holding_avg = (timedelta(minutes=round(losing_trades['trade_duration'].mean())) if not losing_trades.empty else timedelta()) + winstreak, loss_streak = calc_consecutive(results) return { 'wins': len(winning_trades), @@ -287,6 +308,8 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: 'winner_holding_avg_s': winner_holding_avg.total_seconds(), 'loser_holding_avg': loser_holding_avg, 'loser_holding_avg_s': loser_holding_avg.total_seconds(), + 'max_consecutive_wins': winstreak, + 'max_consecutive_losses': loss_streak, } From 380244f8b10ae4e09dd55cb868daf754cbf0c6c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jul 2023 07:09:11 +0200 Subject: [PATCH 4/7] Improve calc_streak, rename method --- tests/optimize/test_optimize_reports.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 4ff8a981d..2ea6a33a4 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -24,7 +24,7 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera store_backtest_stats, text_table_bt_results, text_table_exit_reason, text_table_strategy) from freqtrade.optimize.optimize_reports.optimize_reports import (_get_resample_from_period, - calc_consecutive) + calc_streak) from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.util import dt_ts from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc @@ -349,27 +349,30 @@ def test_generate_trading_stats(testdatadir): assert res['losses'] == 0 -def test_calculate_consecutive(testdatadir): +def test_calc_streak(testdatadir): df = pd.DataFrame({ 'profit_ratio': [0.05, -0.02, -0.03, -0.05, 0.01, 0.02, 0.03, 0.04, -0.02, -0.03], }) # 4 consecutive wins, 3 consecutive losses - assert calc_consecutive(df) == (4, 3) + res = calc_streak(df) + assert res == (4, 3) + assert isinstance(res[0], int) + assert isinstance(res[1], int) # invert situation df1 = df.copy() df1['profit_ratio'] = df1['profit_ratio'] * -1 - assert calc_consecutive(df1) == (3, 4) + assert calc_streak(df1) == (3, 4) df_empty = pd.DataFrame({ 'profit_ratio': [], }) assert df_empty.empty - assert calc_consecutive(df_empty) == (0, 0) + assert calc_streak(df_empty) == (0, 0) filename = testdatadir / "backtest_results/backtest-result.json" bt_data = load_backtest_data(filename) - assert calc_consecutive(bt_data) == (7, 18) + assert calc_streak(bt_data) == (7, 18) def test_text_table_exit_reason(): From f26b49ee064aab006ac72ea3c2a891fece9fc4ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jul 2023 07:09:19 +0200 Subject: [PATCH 5/7] Ensure return value is an int, not a np.int --- freqtrade/optimize/optimize_reports/optimize_reports.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index afcdf531c..7f2291d67 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -253,7 +253,7 @@ def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]: return result -def calc_consecutive(dataframe: DataFrame) -> Tuple[int, int]: +def calc_streak(dataframe: DataFrame) -> Tuple[int, int]: """ Calculate consecutive wins and losses :param dataframe: Dataframe containing the trades dataframe, with profit_ratio column @@ -265,8 +265,8 @@ def calc_consecutive(dataframe: DataFrame) -> Tuple[int, int]: df['counter'] = df['streaks'].groupby(df['streaks']).cumcount() + 1 res = df.groupby(df['result']).max() # - cons_wins = res.loc['win', 'counter'] if 'win' in res.index else 0 - cons_losses = res.loc['loss', 'counter'] if 'loss' in res.index else 0 + cons_wins = int(res.loc['win', 'counter']) if 'win' in res.index else 0 + cons_losses = int(res.loc['loss', 'counter']) if 'loss' in res.index else 0 return cons_wins, cons_losses @@ -295,7 +295,7 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: if not winning_trades.empty else timedelta()) loser_holding_avg = (timedelta(minutes=round(losing_trades['trade_duration'].mean())) if not losing_trades.empty else timedelta()) - winstreak, loss_streak = calc_consecutive(results) + winstreak, loss_streak = calc_streak(results) return { 'wins': len(winning_trades), From 327b055468bed273cd8b432730ea8ec401ed2030 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jul 2023 07:22:33 +0200 Subject: [PATCH 6/7] Add consecutive wins/losses to backtest output --- docs/backtesting.md | 3 +++ freqtrade/optimize/optimize_reports/bt_output.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index abaf00a53..ccd4ed4ac 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -324,6 +324,7 @@ A backtesting result will look like that: | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Max Consecutive Wins / Loss | 3 / 4 | | Rejected Entry signals | 3089 | | Entry/Exit Timeouts | 0 / 0 | | Canceled Trade Entries | 34 | @@ -428,6 +429,7 @@ It contains some useful key metrics about performance of your strategy on backte | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Max Consecutive Wins / Loss | 3 / 4 | | Rejected Entry signals | 3089 | | Entry/Exit Timeouts | 0 / 0 | | Canceled Trade Entries | 34 | @@ -467,6 +469,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. +- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row. - `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached. - `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used). - `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`. diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index d33ae4070..eb30d0c97 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -270,6 +270,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), + ('Max Consecutive Wins / Loss', + f"{strat_results['max_consecutive_wins']} / {strat_results['max_consecutive_losses']}" + if 'max_consecutive_losses' in strat_results else 'N/A'), ('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')), ('Entry/Exit Timeouts', f"{strat_results.get('timedout_entry_orders', 'N/A')} / " From 47fca02ba04e06e7eb27b56ff0b74415a7c9b8e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 Jul 2023 07:06:42 +0200 Subject: [PATCH 7/7] Improve docstring --- freqtrade/optimize/optimize_reports/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 7f2291d67..f24e30318 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -255,7 +255,7 @@ def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]: def calc_streak(dataframe: DataFrame) -> Tuple[int, int]: """ - Calculate consecutive wins and losses + Calculate consecutive win and loss streaks :param dataframe: Dataframe containing the trades dataframe, with profit_ratio column :return: Tuple containing consecutive wins and losses """