From 36f1111d92b880704cb501d2d0d1ae5508c92a83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Mar 2024 13:11:59 +0100 Subject: [PATCH 1/5] Remove custom handling for exit reason stats. It's not different from regular tag outputs, really --- .../optimize/optimize_reports/__init__.py | 7 ++- .../optimize/optimize_reports/bt_output.py | 50 +++++-------------- .../optimize_reports/optimize_reports.py | 45 ++--------------- 3 files changed, 21 insertions(+), 81 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/__init__.py b/freqtrade/optimize/optimize_reports/__init__.py index 9e3ac46bc..bb91bf33c 100644 --- a/freqtrade/optimize/optimize_reports/__init__.py +++ b/freqtrade/optimize/optimize_reports/__init__.py @@ -6,13 +6,12 @@ from freqtrade.optimize.optimize_reports.bt_output import (generate_edge_table, show_sorted_pairlist, text_table_add_metrics, text_table_bt_results, - text_table_exit_reason, text_table_periodic_breakdown, text_table_strategy, text_table_tags) from freqtrade.optimize.optimize_reports.bt_storage import (store_backtest_analysis_results, store_backtest_stats) 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_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) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 2a4be7e7a..afef4c4bb 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from tabulate import tabulate @@ -60,32 +60,6 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") -def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: - """ - Generate small table outlining Backtest results - :param exit_reason_stats: Exit reason metrics - :param stake_currency: Stakecurrency used - :return: pretty printed table with tabulate as string - """ - headers = [ - 'Exit Reason', - 'Exits', - 'Win Draws Loss Win%', - 'Avg Profit %', - f'Tot Profit {stake_currency}', - 'Tot Profit %', - ] - - output = [[ - t.get('exit_reason', t.get('sell_reason')), t['trades'], - generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), - t['profit_mean_pct'], - fmt_coin(t['profit_total_abs'], stake_currency, False), - t['profit_total_pct'], - ] for t in exit_reason_stats] - return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") - - def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe @@ -93,20 +67,23 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ + fallback: Optional[str] = None if (tag_type == "enter_tag"): headers = _get_line_header("TAG", stake_currency) else: - headers = _get_line_header("TAG", stake_currency, 'Exits') + headers = _get_line_header("Exit Reason", stake_currency, 'Exits') + fallback = 'exit_reason' + floatfmt = _get_line_floatfmt(stake_currency) output = [ [ - t['key'] if t['key'] is not None and len( - t['key']) > 0 else "OTHER", + t['key'] if t.get('key') is not None and len( + t['key']) > 0 else t.get(fallback, "OTHER"), t['trades'], t['profit_mean_pct'], t['profit_total_abs'], t['profit_total_pct'], - t['duration_avg'], + t.get('duration_avg'), generate_wins_draws_losses( t['wins'], t['draws'], @@ -317,17 +294,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - if (results.get('results_per_enter_tag') is not None): - table = text_table_tags("enter_tag", results['results_per_enter_tag'], stake_currency) + if (enter_tags := results.get('results_per_enter_tag')) is not None: + table = text_table_tags("enter_tag", enter_tags, stake_currency) if isinstance(table, str) and len(table) > 0: print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '=')) print(table) - exit_reasons = results.get('exit_reason_summary') - if exit_reasons: - table = text_table_exit_reason(exit_reason_stats=exit_reasons, - stake_currency=stake_currency) + if (exit_reasons := results.get('exit_reason_summary')) is not None: + table = text_table_tags("exit_tag", exit_reasons, stake_currency) + if isinstance(table, str) and len(table) > 0: print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 47aab2a62..e9c0f44f8 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Tuple, Union import numpy as np from pandas import DataFrame, Series, concat, to_datetime -from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, IntOrInf +from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, calculate_expectancy, calculate_market_change, calculate_max_drawdown, calculate_sharpe, calculate_sortino) @@ -154,42 +154,6 @@ def generate_tag_metrics(tag_type: str, return [] -def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> List[Dict]: - """ - Generate small table outlining Backtest results - :param max_open_trades: Max_open_trades parameter - :param results: Dataframe containing the backtest result for one strategy - :return: List of Dicts containing the metrics per Sell reason - """ - tabular_data = [] - - for reason, count in results['exit_reason'].value_counts().items(): - result = results.loc[results['exit_reason'] == reason] - - profit_mean = result['profit_ratio'].mean() - profit_sum = result['profit_ratio'].sum() - profit_total = profit_sum / max_open_trades - - tabular_data.append( - { - 'exit_reason': reason, - 'trades': count, - 'wins': len(result[result['profit_abs'] > 0]), - 'draws': len(result[result['profit_abs'] == 0]), - 'losses': len(result[result['profit_abs'] < 0]), - 'winrate': len(result[result['profit_abs'] > 0]) / count if count else 0.0, - 'profit_mean': profit_mean, - 'profit_mean_pct': round(profit_mean * 100, 2), - 'profit_sum': profit_sum, - 'profit_sum_pct': round(profit_sum * 100, 2), - 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': profit_total, - 'profit_total_pct': round(profit_total * 100, 2), - } - ) - return tabular_data - - def generate_strategy_comparison(bt_stats: Dict) -> List[Dict]: """ Generate summary per strategy @@ -383,9 +347,10 @@ def generate_strategy_stats(pairlist: List[str], enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance, results=results, skip_nan=False) - - exit_reason_stats = generate_exit_reason_stats(max_open_trades=max_open_trades, - results=results) + exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=start_balance, + results=results, skip_nan=False) + # exit_reason_stats = generate_exit_reason_stats(max_open_trades=max_open_trades, + # results=results) left_open_results = generate_pair_metrics( pairlist, stake_currency=stake_currency, starting_balance=start_balance, results=results.loc[results['exit_reason'] == 'force_exit'], skip_nan=True) From be26e31235d491285c6714410c5fae99b4157c65 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Mar 2024 13:20:43 +0100 Subject: [PATCH 2/5] Remove obsolete code, improve resilience --- freqtrade/optimize/optimize_reports/bt_output.py | 2 +- freqtrade/optimize/optimize_reports/optimize_reports.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index afef4c4bb..5cef3dc57 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -78,7 +78,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr output = [ [ t['key'] if t.get('key') is not None and len( - t['key']) > 0 else t.get(fallback, "OTHER"), + str(t['key'])) > 0 else t.get(fallback, "OTHER"), t['trades'], t['profit_mean_pct'], t['profit_total_abs'], diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index e9c0f44f8..d4b37d4ee 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -349,8 +349,6 @@ def generate_strategy_stats(pairlist: List[str], results=results, skip_nan=False) exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=start_balance, results=results, skip_nan=False) - # exit_reason_stats = generate_exit_reason_stats(max_open_trades=max_open_trades, - # results=results) left_open_results = generate_pair_metrics( pairlist, stake_currency=stake_currency, starting_balance=start_balance, results=results.loc[results['exit_reason'] == 'force_exit'], skip_nan=True) From 1551f9283291b2d6a67e72c1558cd1980a36dba5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Mar 2024 13:20:53 +0100 Subject: [PATCH 3/5] Fix a few tests --- tests/optimize/test_backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8fc42b474..4163e9606 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -1473,7 +1473,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) text_table_mock = MagicMock() - sell_reason_mock = MagicMock() + tag_metrics_mock = MagicMock() strattable_mock = MagicMock() strat_summary = MagicMock() @@ -1483,7 +1483,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): ) mocker.patch.multiple('freqtrade.optimize.optimize_reports.optimize_reports', generate_pair_metrics=MagicMock(), - generate_exit_reason_stats=sell_reason_mock, + generate_tag_metrics=tag_metrics_mock, generate_strategy_comparison=strat_summary, generate_daily_stats=MagicMock(), ) @@ -1508,7 +1508,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): assert backtestmock.call_count == 2 assert text_table_mock.call_count == 4 assert strattable_mock.call_count == 1 - assert sell_reason_mock.call_count == 2 + assert tag_metrics_mock.call_count == 4 assert strat_summary.call_count == 1 # check the logs, that will contain the backtest result From 0906f050e58c612b9eb39b4b1825a574fcbd4158 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Mar 2024 13:28:13 +0100 Subject: [PATCH 4/5] Fix remaining tests --- .../optimize_reports/optimize_reports.py | 3 +- tests/optimize/test_optimize_reports.py | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index d4b37d4ee..1bf73d714 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -71,7 +71,8 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column 'key': first_column, 'trades': len(result), 'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0, - 'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0, + 'profit_mean_pct': round(result['profit_ratio'].mean() * 100.0, 2 + ) if len(result) > 0 else 0.0, 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_total_abs': result['profit_abs'].sum(), diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 57605d038..db360e10d 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -15,16 +15,16 @@ from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backte from freqtrade.edge import PairInfo from freqtrade.enums import ExitType from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, - generate_edge_table, generate_exit_reason_stats, - generate_pair_metrics, + generate_edge_table, generate_pair_metrics, generate_periodic_breakdown_stats, generate_strategy_comparison, generate_trading_stats, show_sorted_pairlist, store_backtest_analysis_results, store_backtest_stats, text_table_bt_results, - text_table_exit_reason, text_table_strategy) + text_table_strategy) +from freqtrade.optimize.optimize_reports.bt_output import text_table_tags from freqtrade.optimize.optimize_reports.optimize_reports import (_get_resample_from_period, - calc_streak) + calc_streak, generate_tag_metrics) from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.util import dt_ts from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc @@ -392,20 +392,21 @@ def test_text_table_exit_reason(): ) result_str = ( - '| Exit Reason | Exits | Win Draws Loss Win% | Avg Profit % |' - ' Tot Profit BTC | Tot Profit % |\n' - '|---------------+---------+--------------------------+----------------+' - '------------------+----------------|\n' - '| roi | 2 | 2 0 0 100 | 15 |' - ' 0.6 | 15 |\n' - '| stop_loss | 1 | 0 0 1 0 | -10 |' - ' -0.2 | -5 |' + '| Exit Reason | Exits | Avg Profit % | Tot Profit BTC | Tot Profit % |' + ' Avg Duration | Win Draw Loss Win% |\n' + '|---------------+---------+----------------+------------------+----------------+' + '----------------+-------------------------|\n' + '| roi | 2 | 15.00 | 0.60000000 | 2.73 |' + ' 0:20:00 | 2 0 0 100 |\n' + '| stop_loss | 1 | -10.00 | -0.20000000 | -0.91 |' + ' 0:10:00 | 0 0 1 0 |\n' + '| TOTAL | 3 | 6.67 | 0.40000000 | 1.82 |' + ' 0:17:00 | 2 0 1 66.7 |' ) - exit_reason_stats = generate_exit_reason_stats(max_open_trades=2, - results=results) - assert text_table_exit_reason(exit_reason_stats=exit_reason_stats, - stake_currency='BTC') == result_str + exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=22, + results=results, skip_nan=False) + assert text_table_tags('exit_tag', exit_reason_stats, 'BTC') == result_str def test_generate_sell_reason_stats(): @@ -423,10 +424,10 @@ def test_generate_sell_reason_stats(): } ) - exit_reason_stats = generate_exit_reason_stats(max_open_trades=2, - results=results) + exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=22, + results=results, skip_nan=False) roi_result = exit_reason_stats[0] - assert roi_result['exit_reason'] == 'roi' + assert roi_result['key'] == 'roi' assert roi_result['trades'] == 2 assert pytest.approx(roi_result['profit_mean']) == 0.15 assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2) @@ -435,7 +436,7 @@ def test_generate_sell_reason_stats(): stop_result = exit_reason_stats[1] - assert stop_result['exit_reason'] == 'stop_loss' + assert stop_result['key'] == 'stop_loss' assert stop_result['trades'] == 1 assert pytest.approx(stop_result['profit_mean']) == -0.1 assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) From 7b5e44433392a06518230a3d6abc373dab411610 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Mar 2024 13:36:19 +0100 Subject: [PATCH 5/5] Improve code stability --- freqtrade/optimize/optimize_reports/bt_output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 5cef3dc57..ed90cb945 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from tabulate import tabulate @@ -67,7 +67,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ - fallback: Optional[str] = None + fallback: str = '' if (tag_type == "enter_tag"): headers = _get_line_header("TAG", stake_currency) else: