diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7b62661d3..54e7b806f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -330,7 +330,7 @@ class Backtesting: def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, - enable_protections: bool = False) -> DataFrame: + enable_protections: bool = False) -> Dict[str, Any]: """ Implement backtesting functionality @@ -417,7 +417,13 @@ class Backtesting: trades += self.handle_left_open(open_trades, data=data) self.wallets.update() - return trade_list_to_dataframe(trades) + results = trade_list_to_dataframe(trades) + return { + 'results': results, + 'config': self.strategy.config, + 'locks': PairLocks.get_all_locks(), + 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), + } def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) @@ -457,14 +463,12 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - self.all_results[self.strategy.get_strategy_name()] = { - 'results': results, - 'config': self.strategy.config, - 'locks': PairLocks.get_all_locks(), - 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), + results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), - } + }) + self.all_results[self.strategy.get_strategy_name()] = results + return min_date, max_date def start(self) -> None: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9b3a22236..229b35dd5 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -31,7 +31,6 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats -from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -279,7 +278,7 @@ class Hyperopt: min_date, max_date = get_timerange(processed) - backtesting_results = self.backtesting.backtest( + bt_results = self.backtesting.backtest( processed=processed, start_date=min_date.datetime, end_date=max_date.datetime, @@ -288,17 +287,12 @@ class Hyperopt: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - - bt_result = { - 'results': backtesting_results, - 'config': self.backtesting.strategy.config, - 'locks': PairLocks.get_all_locks(), - 'final_balance': self.backtesting.wallets.get_total( - self.backtesting.strategy.config['stake_currency']), + bt_results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), - } - return self._get_results_dict(bt_result, min_date, max_date, + }) + + return self._get_results_dict(bt_results, min_date, max_date, params_dict, params_details, processed=processed) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 77259df87..ef5426c9a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -274,7 +274,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], """ results: Dict[str, DataFrame] = content['results'] if not isinstance(results, DataFrame): - return + return {} config = content['config'] max_open_trades = min(config['max_open_trades'], len(btdata.keys())) starting_balance = config['dry_run_wallet'] diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 39625978b..e09f02c66 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -514,13 +514,14 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: timerange=timerange) processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) - results = backtesting.backtest( + result = backtesting.backtest( processed=processed, start_date=min_date, end_date=max_date, max_open_trades=10, position_stacking=False, ) + results = result['results'] assert not results.empty assert len(results) == 2 @@ -583,8 +584,8 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None max_open_trades=1, position_stacking=False, ) - assert not results.empty - assert len(results) == 1 + assert not results['results'].empty + assert len(results['results']) == 1 def test_processed(default_conf, mocker, testdatadir) -> None: @@ -623,7 +624,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results for [contour, numres] in tests: - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == numres + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres @pytest.mark.parametrize('protections,contour,expected', [ @@ -648,7 +649,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == expected + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): @@ -662,8 +663,8 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(**backtest_conf) - assert results.empty + result = backtesting.backtest(**backtest_conf) + assert result['results'].empty def test_backtest_only_sell(mocker, default_conf, testdatadir): @@ -677,8 +678,8 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(**backtest_conf) - assert results.empty + result = backtesting.backtest(**backtest_conf) + assert result['results'].empty def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): @@ -690,10 +691,11 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override - results = backtesting.backtest(**backtest_conf) + result = backtesting.backtest(**backtest_conf) # 200 candles in backtest data # won't buy on first (shifted by 1) # 100 buys signals + results = result['results'] assert len(results) == 100 # One trade was force-closed at the end assert len(results.loc[results['is_open']]) == 0 @@ -745,9 +747,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) results = backtesting.backtest(**backtest_conf) # Make sure we have parallel trades - assert len(evaluate_result_multi(results, '5m', 2)) > 0 + assert len(evaluate_result_multi(results['results'], '5m', 2)) > 0 # make sure we don't have trades with more than configured max_open_trades - assert len(evaluate_result_multi(results, '5m', 3)) == 0 + assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 backtest_conf = { 'processed': processed, @@ -757,7 +759,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) 'position_stacking': False, } results = backtesting.backtest(**backtest_conf) - assert len(evaluate_result_multi(results, '5m', 1)) == 0 + assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0 def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): @@ -803,7 +805,12 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) - backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS)) + backtestmock = MagicMock(return_value={ + 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) @@ -867,39 +874,51 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): patch_exchange(mocker) + result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], + 'profit_ratio': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', ], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', ], utc=True), + 'trade_duration': [235, 40], + 'is_open': [False, False], + 'stake_amount': [0.01, 0.01], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.104969, 0.103541], + 'sell_reason': [SellType.ROI, SellType.ROI] + }) + result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], + 'profit_ratio': [0.03, 0.01, 0.1], + 'profit_abs': [0.01, 0.02, 0.2], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', + '2018-01-30 05:30:00'], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', + '2018-01-30 08:30:00'], utc=True), + 'trade_duration': [47, 40, 20], + 'is_open': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], + '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] + }) backtestmock = MagicMock(side_effect=[ - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], - 'profit_ratio': [0.0, 0.0], - 'profit_abs': [0.0, 0.0], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', ], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', ], utc=True), - 'trade_duration': [235, 40], - 'is_open': [False, False], - 'stake_amount': [0.01, 0.01], - '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_ratio': [0.03, 0.01, 0.1], - 'profit_abs': [0.01, 0.02, 0.2], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', - '2018-01-30 05:30:00'], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', - '2018-01-30 08:30:00'], utc=True), - 'trade_duration': [47, 40, 20], - 'is_open': [False, False, False], - 'stake_amount': [0.01, 0.01, 0.01], - '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] - }), + { + 'results': result1, + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + }, + { + 'results': result2, + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + } ]) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC']))