From 9d1ad70bb7687e3c58f33a7e409fc219d6eb067c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 06:44:51 +0200 Subject: [PATCH 01/13] Split optimize generation from printing --- freqtrade/optimize/optimize_reports.py | 176 +++++++++++++++---------- 1 file changed, 104 insertions(+), 72 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 646afb5df..2cdecc2d7 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,7 +1,7 @@ import logging from datetime import timedelta from pathlib import Path -from typing import Dict +from typing import Dict, Tuple, List from pandas import DataFrame from tabulate import tabulate @@ -34,6 +34,65 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame file_dump_json(filename, records) +def _get_line_header(first_column: str, stake_currency: str) -> List[str]: + """ + Generate header lines (goes in line with _generate_result_line()) + """ + return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', + f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', + 'Wins', 'Draws', 'Losses'] + + +def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> List: + """ + Generate One Result line. + Columns are: + first_column + 'Buys', 'Avg Profit %', 'Cum Profit %', f'Tot Profit', + 'Tot Profit %', 'Avg Duration', 'Wins', 'Draws', 'Losses' + """ + return [ + first_column, + len(result.index), + result.profit_percent.mean() * 100.0, + result.profit_percent.sum() * 100.0, + result.profit_abs.sum(), + result.profit_percent.sum() * 100.0 / max_open_trades, + str(timedelta( + minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', + len(result[result.profit_abs > 0]), + len(result[result.profit_abs == 0]), + len(result[result.profit_abs < 0]) + ] + + +def _generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, + results: DataFrame, skip_nan: bool = False) -> Tuple: + """ + Generates and returns a list for the given backtest data and the results dataframe + :param data: Dict of containing data that was used during backtesting. + :param stake_currency: stake-currency - used to correctly name headers + :param max_open_trades: Maximum allowed open trades + :param results: Dataframe containing the backtest results + :param skip_nan: Print "left open" open trades + :return: Tuple of (data, headers, floatfmt) of summarized results. + """ + + floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd') + tabular_data = [] + headers = _get_line_header('Pair', stake_currency) + for pair in data: + result = results[results.pair == pair] + if skip_nan and result.profit_abs.isnull().all(): + continue + + tabular_data.append(_generate_result_line(result, max_open_trades, pair)) + + # Append Total + tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL')) + return tabular_data, headers, floatfmt + + def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame, skip_nan: bool = False) -> str: """ @@ -46,66 +105,21 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra :return: pretty printed table with tabulate as string """ - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') - tabular_data = [] - headers = [ - 'Pair', - 'Buys', - 'Avg Profit %', - 'Cum Profit %', - f'Tot Profit {stake_currency}', - 'Tot Profit %', - 'Avg Duration', - 'Wins', - 'Draws', - 'Losses' - ] - for pair in data: - result = results[results.pair == pair] - if skip_nan and result.profit_abs.isnull().all(): - continue - - tabular_data.append([ - pair, - len(result.index), - result.profit_percent.mean() * 100.0, - result.profit_percent.sum() * 100.0, - result.profit_abs.sum(), - result.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', - len(result[result.profit_abs > 0]), - len(result[result.profit_abs == 0]), - len(result[result.profit_abs < 0]) - ]) - - # Append Total - tabular_data.append([ - 'TOTAL', - len(results.index), - results.profit_percent.mean() * 100.0, - results.profit_percent.sum() * 100.0, - results.profit_abs.sum(), - results.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', - len(results[results.profit_abs > 0]), - len(results[results.profit_abs == 0]), - len(results[results.profit_abs < 0]) - ]) + tabular_data, headers, floatfmt = _generate_pair_results(data, stake_currency, max_open_trades, + results, skip_nan) # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore -def generate_text_table_sell_reason(stake_currency: str, max_open_trades: int, - results: DataFrame) -> str: +def _generate_text_table_sell_reason(stake_currency: str, max_open_trades: int, + results: DataFrame) -> Tuple: """ Generate small table outlining Backtest results :param stake_currency: Stakecurrency used :param max_open_trades: Max_open_trades parameter - :param results: Dataframe containing the backtest results - :return: pretty printed table with tabulate as string + :param results: Dataframe containing the backtest result + :return: Tuple of (List, Headers) containing the summary """ tabular_data = [] headers = [ @@ -141,9 +155,43 @@ def generate_text_table_sell_reason(stake_currency: str, max_open_trades: int, profit_percent_tot, ] ) + return tabular_data, headers + + +def generate_text_table_sell_reason(stake_currency: str, + max_open_trades: int, results: DataFrame) -> str: + """ + Generate small table outlining Backtest results + :param stake_currency: Stakecurrency used + :param max_open_trades: Max_open_trades parameter + :param results: Dataframe containing the backtest result + :return: pretty printed table with tabulate as string + """ + + tabular_data, headers = _generate_text_table_sell_reason(stake_currency, + max_open_trades, results) + return tabulate(tabular_data, headers=headers, tablefmt="orgtbl", stralign="right") +def _generate_strategy_summary(stake_currency: str, max_open_trades: str, + all_results: Dict) -> Tuple[List, List, List]: + """ + Generate summary per strategy + :param stake_currency: stake-currency - used to correctly name headers + :param max_open_trades: Maximum allowed open trades used for backtest + :param all_results: Dict of containing results for all strategies + :return: Tuple of (data, headers, floatfmt) of summarized results. + """ + + floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd') + tabular_data = [] + headers = _get_line_header('Strategy', stake_currency) + for strategy, results in all_results.items(): + tabular_data.append(_generate_result_line(results, max_open_trades, strategy)) + return tabular_data, headers, floatfmt + + def generate_text_table_strategy(stake_currency: str, max_open_trades: str, all_results: Dict) -> str: """ @@ -154,25 +202,8 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str, :return: pretty printed table with tabulate as string """ - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') - tabular_data = [] - headers = ['Strategy', 'Buys', 'Avg Profit %', 'Cum Profit %', - f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Wins', 'Draws', 'Losses'] - for strategy, results in all_results.items(): - tabular_data.append([ - strategy, - len(results.index), - results.profit_percent.mean() * 100.0, - results.profit_percent.sum() * 100.0, - results.profit_abs.sum(), - results.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', - len(results[results.profit_abs > 0]), - len(results[results.profit_abs == 0]), - len(results[results.profit_abs < 0]) - ]) + tabular_data, headers, floatfmt = _generate_strategy_summary(stake_currency, + max_open_trades, all_results) # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore @@ -180,7 +211,7 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str, def generate_edge_table(results: dict) -> str: - floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') + floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd') tabular_data = [] headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio', 'Required Risk Reward', 'Expectancy', 'Total Number of Trades', @@ -233,6 +264,7 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], if isinstance(table, str): print('=' * len(table.splitlines()[0])) print() + if len(all_results) > 1: # Print Strategy summary table table = generate_text_table_strategy(config['stake_currency'], From d17300fd84fb682073ebe01fc4e0e7c076230a20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 07:02:24 +0200 Subject: [PATCH 02/13] Refactor sell reason stats to return a dict --- freqtrade/optimize/optimize_reports.py | 80 +++++++++++++------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 2cdecc2d7..e0c66b7c3 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -112,50 +112,40 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore -def _generate_text_table_sell_reason(stake_currency: str, max_open_trades: int, - results: DataFrame) -> Tuple: +def generate_sell_reason_stats(stake_currency: str, max_open_trades: int, + results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results :param stake_currency: Stakecurrency used :param max_open_trades: Max_open_trades parameter - :param results: Dataframe containing the backtest result - :return: Tuple of (List, Headers) containing the summary + :param results: Dataframe containing the backtest result for one strategy + :return: List of Dicts containing the metrics per Sell reason """ tabular_data = [] - headers = [ - "Sell Reason", - "Sells", - "Wins", - "Draws", - "Losses", - "Avg Profit %", - "Cum Profit %", - f"Tot Profit {stake_currency}", - "Tot Profit %", - ] + for reason, count in results['sell_reason'].value_counts().iteritems(): result = results.loc[results['sell_reason'] == reason] - wins = len(result[result['profit_abs'] > 0]) - draws = len(result[result['profit_abs'] == 0]) - loss = len(result[result['profit_abs'] < 0]) - profit_mean = round(result['profit_percent'].mean() * 100.0, 2) - profit_sum = round(result["profit_percent"].sum() * 100.0, 2) - profit_tot = result['profit_abs'].sum() + + profit_mean = result['profit_percent'].mean() + profit_sum = result["profit_percent"].sum() profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2) + tabular_data.append( - [ - reason.value, - count, - wins, - draws, - loss, - profit_mean, - profit_sum, - profit_tot, - profit_percent_tot, - ] + { + 'sell_reason': reason.value, + 'trades': count, + 'wins': len(result[result['profit_abs'] > 0]), + 'draws': len(result[result['profit_abs'] == 0]), + 'losses': len(result[result['profit_abs'] < 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_pct_total': profit_percent_tot, + } ) - return tabular_data, headers + return tabular_data def generate_text_table_sell_reason(stake_currency: str, @@ -164,14 +154,26 @@ def generate_text_table_sell_reason(stake_currency: str, Generate small table outlining Backtest results :param stake_currency: Stakecurrency used :param max_open_trades: Max_open_trades parameter - :param results: Dataframe containing the backtest result + :param results: Dataframe containing the backtest result for one strategy :return: pretty printed table with tabulate as string """ - - tabular_data, headers = _generate_text_table_sell_reason(stake_currency, - max_open_trades, results) - - return tabulate(tabular_data, headers=headers, tablefmt="orgtbl", stralign="right") + headers = [ + 'Sell Reason', + 'Sells', + 'Wins', + 'Draws', + 'Losses', + 'Avg Profit %', + 'Cum Profit %', + f'Tot Profit {stake_currency}', + 'Tot Profit %', + ] + sell_reason_stats = generate_sell_reason_stats(stake_currency, max_open_trades, results) + output = [[ + t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], + t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'], + ] for t in sell_reason_stats] + return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") def _generate_strategy_summary(stake_currency: str, max_open_trades: str, From 876a9e4f44c599358f2a323b7a0bcf45cf78845e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 07:08:15 +0200 Subject: [PATCH 03/13] finish refactor of sell_reason table --- freqtrade/optimize/optimize_reports.py | 18 +++++++++--------- tests/optimize/test_optimize_reports.py | 8 ++++++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index e0c66b7c3..09c862847 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,7 +1,7 @@ import logging from datetime import timedelta from pathlib import Path -from typing import Dict, Tuple, List +from typing import Dict, Tuple, List, Any from pandas import DataFrame from tabulate import tabulate @@ -112,11 +112,10 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore -def generate_sell_reason_stats(stake_currency: str, max_open_trades: int, +def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results - :param stake_currency: Stakecurrency used :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 @@ -148,8 +147,7 @@ def generate_sell_reason_stats(stake_currency: str, max_open_trades: int, return tabular_data -def generate_text_table_sell_reason(stake_currency: str, - max_open_trades: int, results: DataFrame) -> str: +def generate_text_table_sell_reason(sell_reason_stats: Dict[str, Any], stake_currency: str) -> str: """ Generate small table outlining Backtest results :param stake_currency: Stakecurrency used @@ -168,7 +166,7 @@ def generate_text_table_sell_reason(stake_currency: str, f'Tot Profit {stake_currency}', 'Tot Profit %', ] - sell_reason_stats = generate_sell_reason_stats(stake_currency, max_open_trades, results) + output = [[ t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'], @@ -249,9 +247,11 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table_sell_reason(stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results) + sell_reason_stats = generate_sell_reason_stats(max_open_trades=config['max_open_trades'], + results=results) + table = generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats, + stake_currency=config['stake_currency'], + ) if isinstance(table, str): print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index e0782146a..94c404b38 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -6,6 +6,7 @@ from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( generate_edge_table, generate_text_table, generate_text_table_sell_reason, + generate_sell_reason_stats, generate_text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -65,8 +66,11 @@ def test_generate_text_table_sell_reason(default_conf, mocker): '| stop_loss | 1 | 0 | 0 | 1 |' ' -10 | -10 | -0.2 | -5 |' ) - assert generate_text_table_sell_reason(stake_currency='BTC', max_open_trades=2, - results=results) == result_str + + sell_reason_stats = generate_sell_reason_stats(max_open_trades=2, + results=results) + assert generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats, + stake_currency='BTC') == result_str def test_generate_text_table_strategy(default_conf, mocker): From e1362755d25fd9e7507bdf9640ec5d637b8f6622 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 07:14:21 +0200 Subject: [PATCH 04/13] Add test for sell_reason_stats --- tests/optimize/test_optimize_reports.py | 44 ++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 94c404b38..49b707502 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,13 +1,14 @@ from pathlib import Path import pandas as pd +import pytest from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( - generate_edge_table, generate_text_table, generate_text_table_sell_reason, - generate_sell_reason_stats, - generate_text_table_strategy, store_backtest_result) + generate_edge_table, generate_sell_reason_stats, generate_text_table, + generate_text_table_sell_reason, generate_text_table_strategy, + store_backtest_result) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -41,7 +42,7 @@ def test_generate_text_table(default_conf, mocker): results=results) == result_str -def test_generate_text_table_sell_reason(default_conf, mocker): +def test_generate_text_table_sell_reason(default_conf): results = pd.DataFrame( { @@ -73,6 +74,41 @@ def test_generate_text_table_sell_reason(default_conf, mocker): stake_currency='BTC') == result_str +def test_generate_sell_reason_stats(default_conf): + + results = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, -0.1], + 'profit_abs': [0.2, 0.4, -0.2], + 'trade_duration': [10, 30, 10], + 'wins': [2, 0, 0], + 'draws': [0, 0, 0], + 'losses': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + sell_reason_stats = generate_sell_reason_stats(max_open_trades=2, + results=results) + roi_result = sell_reason_stats[0] + assert roi_result['sell_reason'] == '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) + assert pytest.approx(roi_result['profit_mean']) == 0.15 + assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2) + + stop_result = sell_reason_stats[1] + + assert stop_result['sell_reason'] == '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) + assert pytest.approx(stop_result['profit_mean']) == -0.1 + assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) + + def test_generate_text_table_strategy(default_conf, mocker): results = {} results['TestStrategy1'] = pd.DataFrame( From 0917b17efd9533fabbd0704a732e641f3ba577a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 19:18:53 +0200 Subject: [PATCH 05/13] Refactor result_line to return dict --- freqtrade/optimize/optimize_reports.py | 91 ++++++++++++++++---------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 09c862847..d6072e0cb 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -34,6 +34,13 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame file_dump_json(filename, records) +def _get_line_floatfmt() -> List[str]: + """ + Generate floatformat (goes in line with _generate_result_line()) + """ + return ['s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd'] + + def _get_line_header(first_column: str, stake_currency: str) -> List[str]: """ Generate header lines (goes in line with _generate_result_line()) @@ -43,31 +50,36 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: 'Wins', 'Draws', 'Losses'] -def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> List: +def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict: """ - Generate One Result line. - Columns are: - first_column - 'Buys', 'Avg Profit %', 'Cum Profit %', f'Tot Profit', - 'Tot Profit %', 'Avg Duration', 'Wins', 'Draws', 'Losses' + Generate one result dict, with "first_column" as key. """ - return [ - first_column, - len(result.index), - result.profit_percent.mean() * 100.0, - result.profit_percent.sum() * 100.0, - result.profit_abs.sum(), - result.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', - len(result[result.profit_abs > 0]), - len(result[result.profit_abs == 0]), - len(result[result.profit_abs < 0]) - ] + return { + 'key': first_column, + 'trades': len(result.index), + 'profit_mean': result.profit_percent.mean(), + 'profit_mean_pct': result.profit_percent.mean() * 100.0, + 'profit_sum': result.profit_percent.sum() * 100.0, + 'profit_sum_pct': result.profit_percent.sum() * 100.0, + 'profit_total_abs': result.profit_abs.sum(), + 'profit_total_pct': result.profit_percent.sum() * 100.0 / max_open_trades, + 'duration_avg': str(timedelta( + minutes=round(result.trade_duration.mean())) + ) if not result.empty else '0:00', + # 'duration_max': str(timedelta( + # minutes=round(result.trade_duration.max())) + # ) if not result.empty else '0:00', + # 'duration_min': str(timedelta( + # minutes=round(result.trade_duration.min())) + # ) if not result.empty else '0:00', + 'wins': len(result[result.profit_abs > 0]), + 'draws': len(result[result.profit_abs == 0]), + 'losses': len(result[result.profit_abs < 0]), + } def _generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, - results: DataFrame, skip_nan: bool = False) -> Tuple: + results: DataFrame, skip_nan: bool = False) -> List[Dict]: """ Generates and returns a list for the given backtest data and the results dataframe :param data: Dict of containing data that was used during backtesting. @@ -78,9 +90,8 @@ def _generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_ :return: Tuple of (data, headers, floatfmt) of summarized results. """ - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd') tabular_data = [] - headers = _get_line_header('Pair', stake_currency) + for pair in data: result = results[results.pair == pair] if skip_nan and result.profit_abs.isnull().all(): @@ -90,7 +101,7 @@ def _generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_ # Append Total tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL')) - return tabular_data, headers, floatfmt + return tabular_data def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, @@ -105,10 +116,15 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra :return: pretty printed table with tabulate as string """ - tabular_data, headers, floatfmt = _generate_pair_results(data, stake_currency, max_open_trades, - results, skip_nan) + headers = _get_line_header('Pair', stake_currency) + floatfmt = _get_line_floatfmt() + pair_results = _generate_pair_results(data, stake_currency, max_open_trades, results, skip_nan) + output = [[ + t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + ] for t in pair_results] # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, + return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore @@ -147,7 +163,7 @@ def generate_sell_reason_stats(max_open_trades: int, return tabular_data -def generate_text_table_sell_reason(sell_reason_stats: Dict[str, Any], stake_currency: str) -> str: +def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: """ Generate small table outlining Backtest results :param stake_currency: Stakecurrency used @@ -174,8 +190,8 @@ def generate_text_table_sell_reason(sell_reason_stats: Dict[str, Any], stake_cur return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") -def _generate_strategy_summary(stake_currency: str, max_open_trades: str, - all_results: Dict) -> Tuple[List, List, List]: +def _generate_strategy_summary(stake_currency: str, max_open_trades: int, + all_results: Dict) -> List[Dict]: """ Generate summary per strategy :param stake_currency: stake-currency - used to correctly name headers @@ -184,15 +200,13 @@ def _generate_strategy_summary(stake_currency: str, max_open_trades: str, :return: Tuple of (data, headers, floatfmt) of summarized results. """ - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd') tabular_data = [] - headers = _get_line_header('Strategy', stake_currency) for strategy, results in all_results.items(): tabular_data.append(_generate_result_line(results, max_open_trades, strategy)) - return tabular_data, headers, floatfmt + return tabular_data -def generate_text_table_strategy(stake_currency: str, max_open_trades: str, +def generate_text_table_strategy(stake_currency: str, max_open_trades: int, all_results: Dict) -> str: """ Generate summary table per strategy @@ -201,11 +215,16 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str, :param all_results: Dict of containing results for all strategies :return: pretty printed table with tabulate as string """ + floatfmt = _get_line_floatfmt() + headers = _get_line_header('Strategy', stake_currency) + strategy_results = _generate_strategy_summary(stake_currency, max_open_trades, all_results) - tabular_data, headers, floatfmt = _generate_strategy_summary(stake_currency, - max_open_trades, all_results) + output = [[ + t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + ] for t in strategy_results] # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, + return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore From 18a2dad684617d880e806935073487bb6c5197d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 19:34:46 +0200 Subject: [PATCH 06/13] Extract data generation from generate_text_table --- freqtrade/optimize/optimize_reports.py | 20 ++++++++++---------- tests/optimize/test_optimize_reports.py | 15 +++++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d6072e0cb..ce9faacde 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -104,8 +104,7 @@ def _generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_ return tabular_data -def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, - results: DataFrame, skip_nan: bool = False) -> str: +def generate_text_table(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 :param data: Dict of containing data that was used during backtesting. @@ -118,7 +117,6 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra headers = _get_line_header('Pair', stake_currency) floatfmt = _get_line_floatfmt() - pair_results = _generate_pair_results(data, stake_currency, max_open_trades, results, skip_nan) output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] @@ -259,9 +257,10 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], for strategy, results in all_results.items(): print(f"Result for strategy {strategy}") - table = generate_text_table(btdata, stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results) + pair_results = _generate_pair_results(btdata, stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + results=results, skip_nan=False) + table = generate_text_table(pair_results, stake_currency=config['stake_currency']) if isinstance(table, str): print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) @@ -275,10 +274,11 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table(btdata, - stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results.loc[results.open_at_end], skip_nan=True) + left_open_results = _generate_pair_results(btdata, stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + results=results.loc[results['open_at_end']], + skip_nan=True) + table = generate_text_table(left_open_results, stake_currency=config['stake_currency']) if isinstance(table, str): print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 49b707502..dab0586ac 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -6,9 +6,9 @@ from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( - generate_edge_table, generate_sell_reason_stats, generate_text_table, - generate_text_table_sell_reason, generate_text_table_strategy, - store_backtest_result) + _generate_pair_results, generate_edge_table, generate_sell_reason_stats, + generate_text_table, generate_text_table_sell_reason, + generate_text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -37,9 +37,12 @@ def test_generate_text_table(default_conf, mocker): '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |' ' 15.00 | 0:20:00 | 2 | 0 | 0 |' ) - assert generate_text_table(data={'ETH/BTC': {}}, - stake_currency='BTC', max_open_trades=2, - results=results) == result_str + + pair_results = _generate_pair_results(data={'ETH/BTC': {}}, stake_currency='BTC', + max_open_trades=2, + results=results) + assert generate_text_table(pair_results, + stake_currency='BTC') == result_str def test_generate_text_table_sell_reason(default_conf): From db257e9f7f856d006743972be681a44a9aaa03fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 19:50:09 +0200 Subject: [PATCH 07/13] Rename method to be public --- freqtrade/optimize/optimize_reports.py | 25 +++++++++---------- tests/optimize/test_optimize_reports.py | 32 +++++++++++++++++++++---- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ce9faacde..0f6bc62a1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,7 +1,7 @@ import logging from datetime import timedelta from pathlib import Path -from typing import Dict, Tuple, List, Any +from typing import Dict, List, Any from pandas import DataFrame from tabulate import tabulate @@ -59,7 +59,7 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: 'trades': len(result.index), 'profit_mean': result.profit_percent.mean(), 'profit_mean_pct': result.profit_percent.mean() * 100.0, - 'profit_sum': result.profit_percent.sum() * 100.0, + 'profit_sum': result.profit_percent.sum(), 'profit_sum_pct': result.profit_percent.sum() * 100.0, 'profit_total_abs': result.profit_abs.sum(), 'profit_total_pct': result.profit_percent.sum() * 100.0 / max_open_trades, @@ -78,8 +78,8 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: } -def _generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, - results: DataFrame, skip_nan: bool = False) -> List[Dict]: +def generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, + results: DataFrame, skip_nan: bool = False) -> List[Dict]: """ Generates and returns a list for the given backtest data and the results dataframe :param data: Dict of containing data that was used during backtesting. @@ -161,7 +161,8 @@ def generate_sell_reason_stats(max_open_trades: int, return tabular_data -def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: +def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], + stake_currency: str) -> str: """ Generate small table outlining Backtest results :param stake_currency: Stakecurrency used @@ -257,9 +258,9 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], for strategy, results in all_results.items(): print(f"Result for strategy {strategy}") - pair_results = _generate_pair_results(btdata, stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results, skip_nan=False) + pair_results = generate_pair_results(btdata, stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + results=results, skip_nan=False) table = generate_text_table(pair_results, stake_currency=config['stake_currency']) if isinstance(table, str): print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) @@ -274,10 +275,10 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - left_open_results = _generate_pair_results(btdata, stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results.loc[results['open_at_end']], - skip_nan=True) + left_open_results = generate_pair_results(btdata, stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + results=results.loc[results['open_at_end']], + skip_nan=True) table = generate_text_table(left_open_results, stake_currency=config['stake_currency']) if isinstance(table, str): print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index dab0586ac..006ec4cbb 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -6,7 +6,7 @@ from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( - _generate_pair_results, generate_edge_table, generate_sell_reason_stats, + generate_pair_results, generate_edge_table, generate_sell_reason_stats, generate_text_table, generate_text_table_sell_reason, generate_text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType @@ -38,13 +38,37 @@ def test_generate_text_table(default_conf, mocker): ' 15.00 | 0:20:00 | 2 | 0 | 0 |' ) - pair_results = _generate_pair_results(data={'ETH/BTC': {}}, stake_currency='BTC', - max_open_trades=2, - results=results) + pair_results = generate_pair_results(data={'ETH/BTC': {}}, stake_currency='BTC', + max_open_trades=2, results=results) assert generate_text_table(pair_results, stake_currency='BTC') == result_str +def test_generate_pair_results(default_conf, mocker): + + results = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2], + 'profit_abs': [0.2, 0.4], + 'trade_duration': [10, 30], + 'wins': [2, 0], + 'draws': [0, 0], + 'losses': [0, 0] + } + ) + + pair_results = generate_pair_results(data={'ETH/BTC': {}}, stake_currency='BTC', + max_open_trades=2, results=results) + assert isinstance(pair_results, list) + assert len(pair_results) == 2 + assert pair_results[-1]['key'] == 'TOTAL' + assert ( + pytest.approx(pair_results[-1]['profit_mean_pct']) == pair_results[-1]['profit_mean'] * 100) + assert ( + pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100) + + def test_generate_text_table_sell_reason(default_conf): results = pd.DataFrame( From 027ea64d48aa51a85b0e0062b624a36bd76b4f14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 19:55:02 +0200 Subject: [PATCH 08/13] Fix docstrings, extract strategy-list results --- freqtrade/optimize/optimize_reports.py | 31 +++++++++++-------------- tests/optimize/test_optimize_reports.py | 9 +++++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0f6bc62a1..ea5ed74ba 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -87,7 +87,7 @@ def generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_t :param max_open_trades: Maximum allowed open trades :param results: Dataframe containing the backtest results :param skip_nan: Print "left open" open trades - :return: Tuple of (data, headers, floatfmt) of summarized results. + :return: List of Dicts containing the metrics per pair """ tabular_data = [] @@ -107,11 +107,8 @@ def generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_t def generate_text_table(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 - :param data: Dict of containing data that was used during backtesting. + :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row :param stake_currency: stake-currency - used to correctly name headers - :param max_open_trades: Maximum allowed open trades - :param results: Dataframe containing the backtest results - :param skip_nan: Print "left open" open trades :return: pretty printed table with tabulate as string """ @@ -126,8 +123,7 @@ def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore -def generate_sell_reason_stats(max_open_trades: int, - results: DataFrame) -> List[Dict]: +def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results :param max_open_trades: Max_open_trades parameter @@ -165,9 +161,8 @@ def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: """ Generate small table outlining Backtest results + :param sell_reason_stats: Sell reason metrics :param stake_currency: Stakecurrency used - :param max_open_trades: Max_open_trades parameter - :param results: Dataframe containing the backtest result for one strategy :return: pretty printed table with tabulate as string """ headers = [ @@ -189,14 +184,14 @@ def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") -def _generate_strategy_summary(stake_currency: str, max_open_trades: int, - all_results: Dict) -> List[Dict]: +def generate_strategy_summary(stake_currency: str, max_open_trades: int, + all_results: Dict) -> List[Dict]: """ Generate summary per strategy :param stake_currency: stake-currency - used to correctly name headers :param max_open_trades: Maximum allowed open trades used for backtest :param all_results: Dict of containing results for all strategies - :return: Tuple of (data, headers, floatfmt) of summarized results. + :return: List of Dicts containing the metrics per Strategy """ tabular_data = [] @@ -205,8 +200,7 @@ def _generate_strategy_summary(stake_currency: str, max_open_trades: int, return tabular_data -def generate_text_table_strategy(stake_currency: str, max_open_trades: int, - all_results: Dict) -> str: +def generate_text_table_strategy(strategy_results, stake_currency: str) -> str: """ Generate summary table per strategy :param stake_currency: stake-currency - used to correctly name headers @@ -216,7 +210,6 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: int, """ floatfmt = _get_line_floatfmt() headers = _get_line_header('Strategy', stake_currency) - strategy_results = _generate_strategy_summary(stake_currency, max_open_trades, all_results) output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], @@ -289,9 +282,11 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], if len(all_results) > 1: # Print Strategy summary table - table = generate_text_table_strategy(config['stake_currency'], - config['max_open_trades'], - all_results=all_results) + strategy_results = generate_strategy_summary(stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + all_results=all_results) + + table = generate_text_table_strategy(strategy_results, config['stake_currency']) print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) print('=' * len(table.splitlines()[0])) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 006ec4cbb..6e0dc5441 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -7,7 +7,7 @@ from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( generate_pair_results, generate_edge_table, generate_sell_reason_stats, - generate_text_table, generate_text_table_sell_reason, + generate_text_table, generate_text_table_sell_reason, generate_strategy_summary, generate_text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -173,7 +173,12 @@ def test_generate_text_table_strategy(default_conf, mocker): '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' ' 45.00 | 0:20:00 | 3 | 0 | 0 |' ) - assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str + + strategy_results = generate_strategy_summary(stake_currency='BTC', + max_open_trades=2, + all_results=results) + + assert generate_text_table_strategy(strategy_results, 'BTC') == result_str def test_generate_edge_table(edge_conf, mocker): From 46f1470e280fc75619deffbb8b5a1c19852957c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 20:00:05 +0200 Subject: [PATCH 09/13] Fix failing test --- tests/optimize/test_backtesting.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 019914720..943da7925 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -658,10 +658,17 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) gen_table_mock = MagicMock() - mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table', gen_table_mock) + sell_reason_mock = MagicMock() gen_strattable_mock = MagicMock() - mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table_strategy', - gen_strattable_mock) + gen_strat_summary = MagicMock() + + mocker.patch.multiple('freqtrade.optimize.optimize_reports', + generate_text_table=gen_table_mock, + generate_text_table_strategy=gen_strattable_mock, + generate_pair_results=MagicMock(), + generate_sell_reason_stats=sell_reason_mock, + generate_strategy_summary=gen_strat_summary, + ) patched_configuration_load_config_file(mocker, default_conf) args = [ @@ -683,6 +690,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): assert backtestmock.call_count == 2 assert gen_table_mock.call_count == 4 assert gen_strattable_mock.call_count == 1 + assert sell_reason_mock.call_count == 2 + assert gen_strat_summary.call_count == 1 # check the logs, that will contain the backtest result exists = [ From 462c35cf75b180e1cec80909b05a5ee2cf463e91 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 20:22:22 +0200 Subject: [PATCH 10/13] Move stats generation to the top --- freqtrade/optimize/optimize_reports.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ea5ed74ba..655f79f78 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -249,18 +249,22 @@ def generate_edge_table(results: dict) -> str: def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], all_results: Dict[str, DataFrame]): for strategy, results in all_results.items(): - - print(f"Result for strategy {strategy}") pair_results = generate_pair_results(btdata, stake_currency=config['stake_currency'], max_open_trades=config['max_open_trades'], results=results, skip_nan=False) + sell_reason_stats = generate_sell_reason_stats(max_open_trades=config['max_open_trades'], + results=results) + left_open_results = generate_pair_results(btdata, stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + results=results.loc[results['open_at_end']], + skip_nan=True) + # Print results + print(f"Result for strategy {strategy}") table = generate_text_table(pair_results, stake_currency=config['stake_currency']) if isinstance(table, str): print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - sell_reason_stats = generate_sell_reason_stats(max_open_trades=config['max_open_trades'], - results=results) table = generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats, stake_currency=config['stake_currency'], ) @@ -268,10 +272,6 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - left_open_results = generate_pair_results(btdata, stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results.loc[results['open_at_end']], - skip_nan=True) table = generate_text_table(left_open_results, stake_currency=config['stake_currency']) if isinstance(table, str): print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) From 6a9a8f927e2d7f4560bb4d226f1f7b28f0f01a27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 20:46:31 +0200 Subject: [PATCH 11/13] Rename some methods, improve some testing --- freqtrade/optimize/optimize_reports.py | 10 +-- tests/optimize/test_backtesting.py | 93 ++++++++++++++++++++++++- tests/optimize/test_optimize_reports.py | 12 ++-- 3 files changed, 102 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 655f79f78..b8465ad2e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -78,7 +78,7 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: } -def generate_pair_results(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, +def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame, skip_nan: bool = False) -> List[Dict]: """ Generates and returns a list for the given backtest data and the results dataframe @@ -184,7 +184,7 @@ def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") -def generate_strategy_summary(stake_currency: str, max_open_trades: int, +def generate_strategy_metrics(stake_currency: str, max_open_trades: int, all_results: Dict) -> List[Dict]: """ Generate summary per strategy @@ -249,12 +249,12 @@ def generate_edge_table(results: dict) -> str: def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], all_results: Dict[str, DataFrame]): for strategy, results in all_results.items(): - pair_results = generate_pair_results(btdata, stake_currency=config['stake_currency'], + pair_results = generate_pair_metrics(btdata, stake_currency=config['stake_currency'], max_open_trades=config['max_open_trades'], results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats(max_open_trades=config['max_open_trades'], results=results) - left_open_results = generate_pair_results(btdata, stake_currency=config['stake_currency'], + left_open_results = generate_pair_metrics(btdata, stake_currency=config['stake_currency'], max_open_trades=config['max_open_trades'], results=results.loc[results['open_at_end']], skip_nan=True) @@ -282,7 +282,7 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], if len(all_results) > 1: # Print Strategy summary table - strategy_results = generate_strategy_summary(stake_currency=config['stake_currency'], + strategy_results = generate_strategy_metrics(stake_currency=config['stake_currency'], max_open_trades=config['max_open_trades'], all_results=all_results) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 943da7925..ace82d28b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -665,9 +665,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): mocker.patch.multiple('freqtrade.optimize.optimize_reports', generate_text_table=gen_table_mock, generate_text_table_strategy=gen_strattable_mock, - generate_pair_results=MagicMock(), + generate_pair_metrics=MagicMock(), generate_sell_reason_stats=sell_reason_mock, - generate_strategy_summary=gen_strat_summary, + generate_strategy_metrics=gen_strat_summary, ) patched_configuration_load_config_file(mocker, default_conf) @@ -712,3 +712,92 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): for line in exists: assert log_has(line, caplog) + + +@pytest.mark.filterwarnings("ignore:deprecated") +def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): + + patch_exchange(mocker) + backtestmock = MagicMock(side_effect=[ + pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], + 'profit_percent': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'open_time': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', ], utc=True + ), + 'close_time': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', ], utc=True), + 'open_index': [78, 184], + 'close_index': [125, 192], + 'trade_duration': [235, 40], + 'open_at_end': [False, False], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.104969, 0.103541], + 'sell_reason': [SellType.ROI, SellType.ROI] + }), + pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], + 'profit_percent': [0.03, 0.01, 0.1], + 'profit_abs': [0.01, 0.02, 0.2], + 'open_time': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', + '2018-01-30 05:30:00'], utc=True + ), + 'close_time': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', + '2018-01-30 08:30:00'], utc=True), + 'open_index': [78, 184, 185], + 'close_index': [125, 224, 205], + 'trade_duration': [47, 40, 20], + 'open_at_end': [False, False, False], + 'open_rate': [0.104445, 0.10302485, 0.122541], + 'close_rate': [0.104969, 0.103541, 0.123541], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + }), + ]) + mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['UNITTEST/BTC'])) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) + + patched_configuration_load_config_file(mocker, default_conf) + + args = [ + 'backtesting', + '--config', 'config.json', + '--datadir', str(testdatadir), + '--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'), + '--ticker-interval', '1m', + '--timerange', '1510694220-1510700340', + '--enable-position-stacking', + '--disable-max-market-positions', + '--strategy-list', + 'DefaultStrategy', + 'TestStrategyLegacy', + ] + args = get_args(args) + start_backtesting(args) + + # check the logs, that will contain the backtest result + exists = [ + 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', + 'Parameter --timerange detected: 1510694220-1510700340 ...', + f'Using data directory: {testdatadir} ...', + 'Using stake_currency: BTC ...', + 'Using stake_amount: 0.001 ...', + 'Loading data from 2017-11-14T20:57:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Parameter --enable-position-stacking detected ...', + 'Running backtesting for Strategy DefaultStrategy', + 'Running backtesting for Strategy TestStrategyLegacy', + ] + + for line in exists: + assert log_has(line, caplog) + + captured = capsys.readouterr() + assert 'BACKTESTING REPORT' in captured.out + assert 'SELL REASON STATS' in captured.out + assert 'LEFT OPEN TRADES REPORT' in captured.out + assert 'STRATEGY SUMMARY' in captured.out diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 6e0dc5441..8bef6e2cc 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -6,8 +6,8 @@ from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( - generate_pair_results, generate_edge_table, generate_sell_reason_stats, - generate_text_table, generate_text_table_sell_reason, generate_strategy_summary, + generate_pair_metrics, generate_edge_table, generate_sell_reason_stats, + generate_text_table, generate_text_table_sell_reason, generate_strategy_metrics, generate_text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -38,13 +38,13 @@ def test_generate_text_table(default_conf, mocker): ' 15.00 | 0:20:00 | 2 | 0 | 0 |' ) - pair_results = generate_pair_results(data={'ETH/BTC': {}}, stake_currency='BTC', + pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', max_open_trades=2, results=results) assert generate_text_table(pair_results, stake_currency='BTC') == result_str -def test_generate_pair_results(default_conf, mocker): +def test_generate_pair_metrics(default_conf, mocker): results = pd.DataFrame( { @@ -58,7 +58,7 @@ def test_generate_pair_results(default_conf, mocker): } ) - pair_results = generate_pair_results(data={'ETH/BTC': {}}, stake_currency='BTC', + pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', max_open_trades=2, results=results) assert isinstance(pair_results, list) assert len(pair_results) == 2 @@ -174,7 +174,7 @@ def test_generate_text_table_strategy(default_conf, mocker): ' 45.00 | 0:20:00 | 3 | 0 | 0 |' ) - strategy_results = generate_strategy_summary(stake_currency='BTC', + strategy_results = generate_strategy_metrics(stake_currency='BTC', max_open_trades=2, all_results=results) From abf79e4ab44c1131060bd22d01d70c8c45e0af08 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 May 2020 20:47:48 +0200 Subject: [PATCH 12/13] Use temporary variable to clean up code --- freqtrade/optimize/optimize_reports.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b8465ad2e..4887fe891 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -248,31 +248,34 @@ def generate_edge_table(results: dict) -> str: def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], all_results: Dict[str, DataFrame]): + stake_currency = config['stake_currency'] + max_open_trades = config['max_open_trades'] + for strategy, results in all_results.items(): - pair_results = generate_pair_metrics(btdata, stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], + pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + max_open_trades=max_open_trades, results=results, skip_nan=False) - sell_reason_stats = generate_sell_reason_stats(max_open_trades=config['max_open_trades'], + sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, results=results) - left_open_results = generate_pair_metrics(btdata, stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], + left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + max_open_trades=max_open_trades, results=results.loc[results['open_at_end']], skip_nan=True) # Print results print(f"Result for strategy {strategy}") - table = generate_text_table(pair_results, stake_currency=config['stake_currency']) + table = generate_text_table(pair_results, stake_currency=stake_currency) if isinstance(table, str): print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) table = generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats, - stake_currency=config['stake_currency'], + stake_currency=stake_currency, ) if isinstance(table, str): print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table(left_open_results, stake_currency=config['stake_currency']) + table = generate_text_table(left_open_results, stake_currency=stake_currency) if isinstance(table, str): print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) @@ -282,11 +285,11 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], if len(all_results) > 1: # Print Strategy summary table - strategy_results = generate_strategy_metrics(stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], + strategy_results = generate_strategy_metrics(stake_currency=stake_currency, + max_open_trades=max_open_trades, all_results=all_results) - table = generate_text_table_strategy(strategy_results, config['stake_currency']) + table = generate_text_table_strategy(strategy_results, stake_currency) print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) print('=' * len(table.splitlines()[0])) From 18a5787a2c524d942c19d844f29770ae1a0e5561 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 May 2020 19:17:15 +0200 Subject: [PATCH 13/13] Reorder typing imports Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 4887fe891..1fc4d721e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,7 +1,7 @@ import logging from datetime import timedelta from pathlib import Path -from typing import Dict, List, Any +from typing import Any, Dict, List from pandas import DataFrame from tabulate import tabulate