diff --git a/freqtrade/enums/backteststate.py b/freqtrade/enums/backteststate.py index 4c1bd7cbc..490814497 100644 --- a/freqtrade/enums/backteststate.py +++ b/freqtrade/enums/backteststate.py @@ -8,7 +8,8 @@ class BacktestState(Enum): STARTUP = 1 DATALOAD = 2 ANALYZE = 3 - BACKTEST = 4 + CONVERT = 4 + BACKTEST = 5 def __str__(self): return f"{self.name.lower()}" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9168a0ddb..c8d4efa74 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,6 +21,7 @@ from freqtrade.enums import BacktestState, SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin +from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.persistence import LocalTrade, PairLocks, Trade @@ -118,26 +119,13 @@ class Backtesting: # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) - self._progress = 0 - self._max_steps = 0 - self._action = BacktestState.STARTUP + self.progress = BTProgress() def __del__(self): LoggingMixin.show_output = True PairLocks.use_db = True Trade.use_db = True - def set_progress(self, action: BacktestState, progress: float): - self._progress = 0 - self._action = action - - def get_progress(self) -> float: - - return round(self._progress / self._max_steps, 5) if self._max_steps > 0 else 0 - - def get_action(self) -> str: - return str(self._action) - def _set_strategy(self, strategy: IStrategy): """ Load strategy into backtesting @@ -160,7 +148,7 @@ class Backtesting: Loads backtest data and returns the data combined with the timerange as tuple. """ - self.set_progress(BacktestState.DATALOAD, 0) + self.progress.init_step(BacktestState.DATALOAD, 1) timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) @@ -185,7 +173,7 @@ class Backtesting: timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), self.required_startup, min_date) - self.set_progress(BacktestState.DATALOAD, 1) + self.progress.set_new_value(1) return data, timerange def prepare_backtest(self, enable_protections): @@ -210,8 +198,11 @@ class Backtesting: # and eventually change the constants for indexes at the top headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] data: Dict = {} + self.progress.init_step(BacktestState.CONVERT, len(processed)) + # Create dict with data for pair, pair_data in processed.items(): + self.progress.increment() if not pair_data.empty: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist @@ -422,8 +413,8 @@ class Backtesting: open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trade_count = 0 - self.set_progress(BacktestState.BACKTEST, 0) - self._max_steps = int((end_date - start_date) / timedelta(minutes=self.timeframe_min)) + self.progress.init_step(BacktestState.BACKTEST, int( + (end_date - start_date) / timedelta(minutes=self.timeframe_min))) # Loop timerange and get candle for each pair at that point in time while tmp <= end_date: @@ -484,7 +475,7 @@ class Backtesting: self.protections.global_stop(tmp) # Move time one configured time_interval ahead. - self._progress += 1 + self.progress.increment() tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) @@ -500,8 +491,8 @@ class Backtesting: } def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): - self.set_progress(BacktestState.ANALYZE, 0) - self._max_steps = 0 + self.progress.init_step(BacktestState.ANALYZE, 0) + logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) @@ -528,7 +519,6 @@ class Backtesting: "No data left after adjusting for startup candles.") min_date, max_date = history.get_timerange(preprocessed) - self.set_progress(BacktestState.BACKTEST, 0) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' diff --git a/freqtrade/optimize/bt_progress.py b/freqtrade/optimize/bt_progress.py new file mode 100644 index 000000000..8d4fd1737 --- /dev/null +++ b/freqtrade/optimize/bt_progress.py @@ -0,0 +1,34 @@ + +from freqtrade.enums import BacktestState + + +class BTProgress: + _action: BacktestState = BacktestState.STARTUP + _progress: float = 0 + _max_steps: float = 0 + + def __init__(self): + pass + + def init_step(self, action: BacktestState, max_steps: float): + self._action = action + self._max_steps = max_steps + self._proress = 0 + + def set_new_value(self, new_value: float): + self._progress = new_value + + def increment(self): + self._progress += 1 + + @property + def progress(self): + """ + Get progress as ratio, capped to be between 0 and 1 (to avoid small calculation errors). + """ + return max(min(round(self._progress / self._max_steps, 5) + if self._max_steps > 0 else 0, 1), 0) + + @property + def action(self): + return str(self._action) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index fd3cb345e..0e3b16baf 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -291,6 +291,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac if (not ApiServer._bt or lastconfig.get('timeframe') != strat.timeframe + or lastconfig.get('stake_amount') != btconfig.get('stake_amount') or lastconfig.get('enable_protections') != btconfig.get('enable_protections') or lastconfig.get('protections') != btconfig.get('protections', []) or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)): @@ -298,11 +299,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) # Reset data if backtesting is reloaded - # TODO: is this always necessary?? - ApiServer._backtestdata = None if (not ApiServer._backtestdata or not ApiServer._bt_timerange - or lastconfig.get('timerange') != btconfig['timerange']): + or lastconfig.get('timerange') != btconfig['timerange'] + or lastconfig.get('timeframe') != strat.timeframe): lastconfig['timerange'] = btconfig['timerange'] lastconfig['protections'] = btconfig.get('protections', []) lastconfig['enable_protections'] = btconfig.get('enable_protections') @@ -344,8 +344,8 @@ def api_get_backtest(): return { "status": "running", "running": True, - "step": ApiServer._bt.get_action() if ApiServer._bt else str(BacktestState.STARTUP), - "progress": ApiServer._bt.get_progress() if ApiServer._bt else 0, + "step": ApiServer._bt.progress.action if ApiServer._bt else str(BacktestState.STARTUP), + "progress": ApiServer._bt.progress.progress if ApiServer._bt else 0, "trade_count": len(LocalTrade.trades), "status_msg": "Backtest running", }