From 18a53f446762016b42b15d0adb90181c69a80027 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 07:26:43 +0100 Subject: [PATCH 1/6] Extract generate_text_table from backtesting class --- freqtrade/optimize/backtest_reports.py | 53 +++++++++++++++++++++++ freqtrade/optimize/backtesting.py | 60 ++++---------------------- tests/optimize/test_backtesting.py | 9 ++-- 3 files changed, 68 insertions(+), 54 deletions(-) create mode 100644 freqtrade/optimize/backtest_reports.py diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py new file mode 100644 index 000000000..501d22228 --- /dev/null +++ b/freqtrade/optimize/backtest_reports.py @@ -0,0 +1,53 @@ +from datetime import timedelta +from typing import Dict + +from pandas import DataFrame +from tabulate import tabulate + + +def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, + results: DataFrame, skip_nan: bool = False) -> str: + """ + Generates and returns a text table for the given backtest data and the results dataframe + :return: pretty printed table with tabulate as str + """ + + floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') + tabular_data = [] + headers = ['pair', 'buy count', 'avg profit %', 'cum profit %', + f'tot profit {stake_currency}', 'tot profit %', 'avg duration', + 'profit', 'loss'] + 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]) + ]) + + # 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]) + ]) + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(tabular_data, headers=headers, + floatfmt=floatfmt, tablefmt="pipe") # type: ignore diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9bd0327e0..e90a4c7ea 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,6 +14,7 @@ from tabulate import tabulate from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) +from freqtrade.optimize.backtest_reports import generate_text_table from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException @@ -129,55 +130,6 @@ class Backtesting: return data, timerange - def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame, - skip_nan: bool = False) -> str: - """ - Generates and returns a text table for the given backtest data and the results dataframe - :return: pretty printed table with tabulate as str - """ - stake_currency = str(self.config.get('stake_currency')) - max_open_trades = self.config.get('max_open_trades') - - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') - tabular_data = [] - headers = ['pair', 'buy count', 'avg profit %', 'cum profit %', - 'tot profit ' + stake_currency, 'tot profit %', 'avg duration', - 'profit', 'loss'] - 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]) - ]) - - # 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]) - ]) - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, - floatfmt=floatfmt, tablefmt="pipe") # type: ignore - def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str: """ Generate small table outlining Backtest results @@ -509,13 +461,19 @@ class Backtesting: print(f"Result for strategy {strategy}") print(' BACKTESTING REPORT '.center(133, '=')) - print(self._generate_text_table(data, results)) + print(generate_text_table(data, + stake_currency=self.config['stake_currency'], + max_open_trades=self.config['max_open_trades'], + results=results)) print(' SELL REASON STATS '.center(133, '=')) print(self._generate_text_table_sell_reason(data, results)) print(' LEFT OPEN TRADES REPORT '.center(133, '=')) - print(self._generate_text_table(data, results.loc[results.open_at_end], True)) + print(generate_text_table(data, + stake_currency=self.config['stake_currency'], + max_open_trades=self.config['max_open_trades'], + results=results.loc[results.open_at_end], skip_nan=True)) print() if len(all_results) > 1: # Print Strategy summary table diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4e2fd01cf..67332c10a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -19,6 +19,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting +from freqtrade.optimize.backtest_reports import generate_text_table from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -360,8 +361,8 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: def test_generate_text_table(default_conf, mocker): patch_exchange(mocker) - default_conf['max_open_trades'] = 2 - backtesting = Backtesting(default_conf) + # default_conf['max_open_trades'] = 2 + # backtesting = Backtesting(default_conf) results = pd.DataFrame( { @@ -384,7 +385,9 @@ def test_generate_text_table(default_conf, mocker): '| TOTAL | 2 | 15.00 | 30.00 | ' '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |' ) - assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str + assert generate_text_table(data={'ETH/BTC': {}}, + stake_currency='BTC', max_open_trades=2, + results=results) == result_str def test_generate_text_table_sell_reason(default_conf, mocker): From caec345c0b77364412ae0ec2c524ff2eab9ef316 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 07:28:30 +0100 Subject: [PATCH 2/6] Extract generate_text_table_sell_reason from backtesting class --- freqtrade/optimize/backtest_reports.py | 13 +++++++++++++ freqtrade/optimize/backtesting.py | 17 ++++------------- tests/optimize/test_backtesting.py | 8 +++----- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py index 501d22228..8912af22a 100644 --- a/freqtrade/optimize/backtest_reports.py +++ b/freqtrade/optimize/backtest_reports.py @@ -51,3 +51,16 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") # type: ignore + + +def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str: + """ + Generate small table outlining Backtest results + """ + tabular_data = [] + headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] + for reason, count in results['sell_reason'].value_counts().iteritems(): + profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)]) + loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) + tabular_data.append([reason.value, count, profit, loss]) + return tabulate(tabular_data, headers=headers, tablefmt="pipe") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e90a4c7ea..1c92860a8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,12 +14,13 @@ from tabulate import tabulate from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) -from freqtrade.optimize.backtest_reports import generate_text_table from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import file_dump_json +from freqtrade.optimize.backtest_reports import ( + generate_text_table, generate_text_table_sell_reason) from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -130,17 +131,7 @@ class Backtesting: return data, timerange - def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str: - """ - Generate small table outlining Backtest results - """ - tabular_data = [] - headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] - for reason, count in results['sell_reason'].value_counts().iteritems(): - profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)]) - loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) - tabular_data.append([reason.value, count, profit, loss]) - return tabulate(tabular_data, headers=headers, tablefmt="pipe") + def _generate_text_table_strategy(self, all_results: dict) -> str: """ @@ -467,7 +458,7 @@ class Backtesting: results=results)) print(' SELL REASON STATS '.center(133, '=')) - print(self._generate_text_table_sell_reason(data, results)) + print(generate_text_table_sell_reason(data, results)) print(' LEFT OPEN TRADES REPORT '.center(133, '=')) print(generate_text_table(data, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 67332c10a..554d8974b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -19,7 +19,8 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting -from freqtrade.optimize.backtest_reports import generate_text_table +from freqtrade.optimize.backtest_reports import ( + generate_text_table, generate_text_table_sell_reason) from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -361,8 +362,6 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: def test_generate_text_table(default_conf, mocker): patch_exchange(mocker) - # default_conf['max_open_trades'] = 2 - # backtesting = Backtesting(default_conf) results = pd.DataFrame( { @@ -392,7 +391,6 @@ def test_generate_text_table(default_conf, mocker): def test_generate_text_table_sell_reason(default_conf, mocker): patch_exchange(mocker) - backtesting = Backtesting(default_conf) results = pd.DataFrame( { @@ -412,7 +410,7 @@ def test_generate_text_table_sell_reason(default_conf, mocker): '| roi | 2 | 2 | 0 |\n' '| stop_loss | 1 | 0 | 1 |' ) - assert backtesting._generate_text_table_sell_reason( + assert generate_text_table_sell_reason( data={'ETH/BTC': {}}, results=results) == result_str From 904e1647e1579f2bc0ff396937ea6982afd4ccd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 07:32:12 +0100 Subject: [PATCH 3/6] Extract generate_text_table_strategy to seperate module --- freqtrade/optimize/backtest_reports.py | 29 +++++++++++++++++++ freqtrade/optimize/backtesting.py | 39 ++++---------------------- tests/optimize/test_backtesting.py | 15 +++------- 3 files changed, 38 insertions(+), 45 deletions(-) diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py index 8912af22a..8f0436563 100644 --- a/freqtrade/optimize/backtest_reports.py +++ b/freqtrade/optimize/backtest_reports.py @@ -64,3 +64,32 @@ def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) - loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) tabular_data.append([reason.value, count, profit, loss]) return tabulate(tabular_data, headers=headers, tablefmt="pipe") + + +def generate_text_table_strategy(stake_currency: str, max_open_trades: str, + all_results: Dict) -> str: + """ + Generate summary table per strategy + """ + + floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') + tabular_data = [] + headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %', + f'tot profit {stake_currency}', 'tot profit %', 'avg duration', + 'profit', 'loss'] + 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]) + ]) + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(tabular_data, headers=headers, + floatfmt=floatfmt, tablefmt="pipe") # type: ignore diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1c92860a8..ae3fbed0a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,7 +10,6 @@ from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame -from tabulate import tabulate from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) @@ -20,7 +19,8 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import file_dump_json from freqtrade.optimize.backtest_reports import ( - generate_text_table, generate_text_table_sell_reason) + generate_text_table, generate_text_table_sell_reason, + generate_text_table_strategy) from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -131,37 +131,6 @@ class Backtesting: return data, timerange - - - def _generate_text_table_strategy(self, all_results: dict) -> str: - """ - Generate summary table per strategy - """ - stake_currency = str(self.config.get('stake_currency')) - max_open_trades = self.config.get('max_open_trades') - - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') - tabular_data = [] - headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %', - 'tot profit ' + stake_currency, 'tot profit %', 'avg duration', - 'profit', 'loss'] - 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]) - ]) - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, - floatfmt=floatfmt, tablefmt="pipe") # type: ignore - def _store_backtest_result(self, recordfilename: Path, results: DataFrame, strategyname: Optional[str] = None) -> None: @@ -469,5 +438,7 @@ class Backtesting: if len(all_results) > 1: # Print Strategy summary table print(' Strategy Summary '.center(133, '=')) - print(self._generate_text_table_strategy(all_results)) + print(generate_text_table_strategy(self.config['stake_currency'], + self.config['max_open_trades'], + all_results=all_results)) print('\nFor more details, please look at the detail tables above') diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 554d8974b..57b80f837 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -20,7 +20,8 @@ from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting from freqtrade.optimize.backtest_reports import ( - generate_text_table, generate_text_table_sell_reason) + generate_text_table, generate_text_table_sell_reason, + generate_text_table_strategy) from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -361,7 +362,6 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: def test_generate_text_table(default_conf, mocker): - patch_exchange(mocker) results = pd.DataFrame( { @@ -390,7 +390,6 @@ def test_generate_text_table(default_conf, mocker): def test_generate_text_table_sell_reason(default_conf, mocker): - patch_exchange(mocker) results = pd.DataFrame( { @@ -414,13 +413,7 @@ def test_generate_text_table_sell_reason(default_conf, mocker): data={'ETH/BTC': {}}, results=results) == result_str -def test_generate_text_table_strategyn(default_conf, mocker): - """ - Test Backtesting.generate_text_table_sell_reason() method - """ - patch_exchange(mocker) - default_conf['max_open_trades'] = 2 - backtesting = Backtesting(default_conf) +def test_generate_text_table_strategy(default_conf, mocker): results = {} results['ETH/BTC'] = pd.DataFrame( { @@ -455,7 +448,7 @@ def test_generate_text_table_strategyn(default_conf, mocker): '| LTC/BTC | 3 | 30.00 | 90.00 ' '| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |' ) - assert backtesting._generate_text_table_strategy(all_results=results) == result_str + assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: From 10ee23622a34797170e2ad37b3389b7b1ea7ebfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 07:33:55 +0100 Subject: [PATCH 4/6] Extract tests for backtest_reports to their own test module --- tests/optimize/test_backtest_reports.py | 96 +++++++++++++++++++++++++ tests/optimize/test_backtesting.py | 93 ------------------------ 2 files changed, 96 insertions(+), 93 deletions(-) create mode 100644 tests/optimize/test_backtest_reports.py diff --git a/tests/optimize/test_backtest_reports.py b/tests/optimize/test_backtest_reports.py new file mode 100644 index 000000000..726202517 --- /dev/null +++ b/tests/optimize/test_backtest_reports.py @@ -0,0 +1,96 @@ +import pandas as pd + +from freqtrade.optimize.backtest_reports import ( + generate_text_table, generate_text_table_sell_reason, + generate_text_table_strategy) +from freqtrade.strategy.interface import SellType + + +def test_generate_text_table(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], + 'profit': [2, 0], + 'loss': [0, 0] + } + ) + + result_str = ( + '| pair | buy count | avg profit % | cum profit % | ' + 'tot profit BTC | tot profit % | avg duration | profit | loss |\n' + '|:--------|------------:|---------------:|---------------:|' + '-----------------:|---------------:|:---------------|---------:|-------:|\n' + '| ETH/BTC | 2 | 15.00 | 30.00 | ' + '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n' + '| TOTAL | 2 | 15.00 | 30.00 | ' + '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |' + ) + assert generate_text_table(data={'ETH/BTC': {}}, + stake_currency='BTC', max_open_trades=2, + results=results) == result_str + + +def test_generate_text_table_sell_reason(default_conf, mocker): + + results = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, -0.3], + 'profit_abs': [0.2, 0.4, -0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + result_str = ( + '| Sell Reason | Count | Profit | Loss |\n' + '|:--------------|--------:|---------:|-------:|\n' + '| roi | 2 | 2 | 0 |\n' + '| stop_loss | 1 | 0 | 1 |' + ) + assert generate_text_table_sell_reason( + data={'ETH/BTC': {}}, results=results) == result_str + + +def test_generate_text_table_strategy(default_conf, mocker): + results = {} + results['ETH/BTC'] = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, 0.3], + 'profit_abs': [0.2, 0.4, 0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + results['LTC/BTC'] = pd.DataFrame( + { + 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], + 'profit_percent': [0.4, 0.2, 0.3], + 'profit_abs': [0.4, 0.4, 0.5], + 'trade_duration': [15, 30, 15], + 'profit': [4, 1, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + result_str = ( + '| Strategy | buy count | avg profit % | cum profit % ' + '| tot profit BTC | tot profit % | avg duration | profit | loss |\n' + '|:-----------|------------:|---------------:|---------------:' + '|-----------------:|---------------:|:---------------|---------:|-------:|\n' + '| ETH/BTC | 3 | 20.00 | 60.00 ' + '| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n' + '| LTC/BTC | 3 | 30.00 | 90.00 ' + '| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |' + ) + assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 57b80f837..9f31114b4 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -19,9 +19,6 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting -from freqtrade.optimize.backtest_reports import ( - generate_text_table, generate_text_table_sell_reason, - generate_text_table_strategy) from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -361,96 +358,6 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC']) -def test_generate_text_table(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], - 'profit': [2, 0], - 'loss': [0, 0] - } - ) - - result_str = ( - '| pair | buy count | avg profit % | cum profit % | ' - 'tot profit BTC | tot profit % | avg duration | profit | loss |\n' - '|:--------|------------:|---------------:|---------------:|' - '-----------------:|---------------:|:---------------|---------:|-------:|\n' - '| ETH/BTC | 2 | 15.00 | 30.00 | ' - '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n' - '| TOTAL | 2 | 15.00 | 30.00 | ' - '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |' - ) - assert generate_text_table(data={'ETH/BTC': {}}, - stake_currency='BTC', max_open_trades=2, - results=results) == result_str - - -def test_generate_text_table_sell_reason(default_conf, mocker): - - results = pd.DataFrame( - { - 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, -0.3], - 'profit_abs': [0.2, 0.4, -0.5], - 'trade_duration': [10, 30, 10], - 'profit': [2, 0, 0], - 'loss': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - } - ) - - result_str = ( - '| Sell Reason | Count | Profit | Loss |\n' - '|:--------------|--------:|---------:|-------:|\n' - '| roi | 2 | 2 | 0 |\n' - '| stop_loss | 1 | 0 | 1 |' - ) - assert generate_text_table_sell_reason( - data={'ETH/BTC': {}}, results=results) == result_str - - -def test_generate_text_table_strategy(default_conf, mocker): - results = {} - results['ETH/BTC'] = pd.DataFrame( - { - 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, 0.3], - 'profit_abs': [0.2, 0.4, 0.5], - 'trade_duration': [10, 30, 10], - 'profit': [2, 0, 0], - 'loss': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - } - ) - results['LTC/BTC'] = pd.DataFrame( - { - 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], - 'profit_percent': [0.4, 0.2, 0.3], - 'profit_abs': [0.4, 0.4, 0.5], - 'trade_duration': [15, 30, 15], - 'profit': [4, 1, 0], - 'loss': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - } - ) - - result_str = ( - '| Strategy | buy count | avg profit % | cum profit % ' - '| tot profit BTC | tot profit % | avg duration | profit | loss |\n' - '|:-----------|------------:|---------------:|---------------:' - '|-----------------:|---------------:|:---------------|---------:|-------:|\n' - '| ETH/BTC | 3 | 20.00 | 60.00 ' - '| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n' - '| LTC/BTC | 3 | 30.00 | 90.00 ' - '| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |' - ) - assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str - - def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) From 8cc48cf4b042f4b47ad3902addd2fafc5e433762 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 09:31:35 +0100 Subject: [PATCH 5/6] Fix tests where mocks fail now --- tests/optimize/test_backtesting.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 9f31114b4..83d212e3d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -366,11 +366,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.optimize.backtesting.Backtesting', - backtest=MagicMock(), - _generate_text_table=MagicMock(return_value='1'), - ) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = '1m' @@ -399,11 +396,8 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.optimize.backtesting.Backtesting', - backtest=MagicMock(), - _generate_text_table=MagicMock(return_value='1'), - ) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = "1m" @@ -714,7 +708,8 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker, api_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) - mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock()) + patched_configuration_load_config_file(mocker, default_conf) args = [ @@ -760,10 +755,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): backtestmock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) gen_table_mock = MagicMock() - mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table', gen_table_mock) gen_strattable_mock = MagicMock() - mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', - gen_strattable_mock) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table_strategy', gen_strattable_mock) patched_configuration_load_config_file(mocker, default_conf) args = [ From a9fbad0741fe7475a2d0b90c22788a42a24e3a05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 09:37:54 +0100 Subject: [PATCH 6/6] Improve docstrings --- freqtrade/optimize/backtest_reports.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py index 8f0436563..5778747cf 100644 --- a/freqtrade/optimize/backtest_reports.py +++ b/freqtrade/optimize/backtest_reports.py @@ -9,7 +9,12 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra results: DataFrame, skip_nan: bool = False) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe - :return: pretty printed table with tabulate as str + :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: pretty printed table with tabulate as string """ floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') @@ -56,6 +61,9 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str: """ Generate small table outlining Backtest results + :param data: Dict of containing data that was used during backtesting. + :param results: Dataframe containing the backtest results + :return: pretty printed table with tabulate as string """ tabular_data = [] headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] @@ -70,6 +78,10 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str, all_results: Dict) -> str: """ Generate summary table 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: pretty printed table with tabulate as string """ floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')