From 048008756ff8390c34f32a583bef05d1205e079d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Mar 2021 19:16:18 +0100 Subject: [PATCH] Add progress tracking for backtesting --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/backteststate.py | 14 +++++++++++++ freqtrade/optimize/backtesting.py | 28 ++++++++++++++++++++++++- freqtrade/rpc/api_server/api_schemas.py | 2 ++ freqtrade/rpc/api_server/api_v1.py | 14 +++++++++++-- 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 freqtrade/enums/backteststate.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 78163d86f..ac5f804c9 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa: F401 +from freqtrade.enums.backteststate import BacktestState from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType diff --git a/freqtrade/enums/backteststate.py b/freqtrade/enums/backteststate.py new file mode 100644 index 000000000..4c1bd7cbc --- /dev/null +++ b/freqtrade/enums/backteststate.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class BacktestState(Enum): + """ + Bot application states + """ + STARTUP = 1 + DATALOAD = 2 + ANALYZE = 3 + BACKTEST = 4 + + def __str__(self): + return f"{self.name.lower()}" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f883c0cbf..9168a0ddb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe from freqtrade.data.converter import trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import SellType +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 @@ -118,11 +118,26 @@ 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 + 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 @@ -145,6 +160,8 @@ class Backtesting: Loads backtest data and returns the data combined with the timerange as tuple. """ + self.set_progress(BacktestState.DATALOAD, 0) + timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) @@ -168,6 +185,7 @@ class Backtesting: timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), self.required_startup, min_date) + self.set_progress(BacktestState.DATALOAD, 1) return data, timerange def prepare_backtest(self, enable_protections): @@ -404,6 +422,9 @@ 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)) + # Loop timerange and get candle for each pair at that point in time while tmp <= end_date: open_trade_count_start = open_trade_count @@ -463,6 +484,7 @@ class Backtesting: self.protections.global_stop(tmp) # Move time one configured time_interval ahead. + self._progress += 1 tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) @@ -478,6 +500,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 logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) @@ -504,6 +528,8 @@ 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)} ' f'({(max_date - min_date).days} days).') diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bf5382131..9a4ac5cd0 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -329,5 +329,7 @@ class BacktestResponse(BaseModel): status: str running: bool status_msg: str + step: str + progress: float # TODO: Properly type backtestresult... backtest_result: Optional[Dict[str, Any]] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index d4f138ee8..30b4c998c 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -23,6 +23,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, BacktestReques WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException +from freqtrade.state import BacktestState logger = logging.getLogger(__name__) @@ -292,8 +293,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac or lastconfig.get('timeframe') != strat.timeframe 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) - ): + or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)): # TODO: Investigate if enabling protections can be dynamically ingested from here... from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) @@ -327,6 +327,8 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac return { "status": "running", "running": True, + "progress": 0, + "step": str(BacktestState.STARTUP), "status_msg": "Backtest started", } @@ -341,6 +343,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, "status_msg": "Backtest running", } @@ -348,6 +352,8 @@ def api_get_backtest(): return { "status": "not_started", "running": False, + "step": "", + "progress": 0, "status_msg": "Backtesting not yet executed" } @@ -355,6 +361,8 @@ def api_get_backtest(): "status": "ended", "running": False, "status_msg": "Backtest ended", + "step": "finished", + "progress": 1, "backtest_result": ApiServer._bt.results, } @@ -366,6 +374,7 @@ def api_delete_backtest(): return { "status": "running", "running": True, + "progress": 0, "status_msg": "Backtest running", } if ApiServer._bt: @@ -377,5 +386,6 @@ def api_delete_backtest(): return { "status": "reset", "running": False, + "progress": 0, "status_msg": "Backtesting reset", }