Add progress tracking for backtesting

This commit is contained in:
Matthias 2021-03-11 19:16:18 +01:00
parent 06b6726029
commit 048008756f
5 changed files with 56 additions and 3 deletions

View File

@ -1,4 +1,5 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.enums.backteststate import BacktestState
from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType from freqtrade.enums.selltype import SellType

View File

@ -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()}"

View File

@ -17,7 +17,7 @@ from freqtrade.data import history
from freqtrade.data.btanalysis import trade_list_to_dataframe from freqtrade.data.btanalysis import trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframes from freqtrade.data.converter import trim_dataframes
from freqtrade.data.dataprovider import DataProvider 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.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
@ -118,11 +118,26 @@ class Backtesting:
# Get maximum required startup period # Get maximum required startup period
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) 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): def __del__(self):
LoggingMixin.show_output = True LoggingMixin.show_output = True
PairLocks.use_db = True PairLocks.use_db = True
Trade.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): def _set_strategy(self, strategy: IStrategy):
""" """
Load strategy into backtesting Load strategy into backtesting
@ -145,6 +160,8 @@ class Backtesting:
Loads backtest data and returns the data combined with the timerange Loads backtest data and returns the data combined with the timerange
as tuple. as tuple.
""" """
self.set_progress(BacktestState.DATALOAD, 0)
timerange = TimeRange.parse_timerange(None if self.config.get( timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) '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), timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
self.required_startup, min_date) self.required_startup, min_date)
self.set_progress(BacktestState.DATALOAD, 1)
return data, timerange return data, timerange
def prepare_backtest(self, enable_protections): def prepare_backtest(self, enable_protections):
@ -404,6 +422,9 @@ class Backtesting:
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
open_trade_count = 0 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 # Loop timerange and get candle for each pair at that point in time
while tmp <= end_date: while tmp <= end_date:
open_trade_count_start = open_trade_count open_trade_count_start = open_trade_count
@ -463,6 +484,7 @@ class Backtesting:
self.protections.global_stop(tmp) self.protections.global_stop(tmp)
# Move time one configured time_interval ahead. # Move time one configured time_interval ahead.
self._progress += 1
tmp += timedelta(minutes=self.timeframe_min) tmp += timedelta(minutes=self.timeframe_min)
trades += self.handle_left_open(open_trades, data=data) 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): 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()) logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
backtest_start_time = datetime.now(timezone.utc) backtest_start_time = datetime.now(timezone.utc)
self._set_strategy(strat) self._set_strategy(strat)
@ -504,6 +528,8 @@ class Backtesting:
"No data left after adjusting for startup candles.") "No data left after adjusting for startup candles.")
min_date, max_date = history.get_timerange(preprocessed) 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)} ' logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(max_date - min_date).days} days).') f'({(max_date - min_date).days} days).')

View File

@ -329,5 +329,7 @@ class BacktestResponse(BaseModel):
status: str status: str
running: bool running: bool
status_msg: str status_msg: str
step: str
progress: float
# TODO: Properly type backtestresult... # TODO: Properly type backtestresult...
backtest_result: Optional[Dict[str, Any]] backtest_result: Optional[Dict[str, Any]]

View File

@ -23,6 +23,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, BacktestReques
WhitelistResponse) WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
from freqtrade.state import BacktestState
logger = logging.getLogger(__name__) 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('timeframe') != strat.timeframe
or lastconfig.get('enable_protections') != btconfig.get('enable_protections') or lastconfig.get('enable_protections') != btconfig.get('enable_protections')
or lastconfig.get('protections') != btconfig.get('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... # TODO: Investigate if enabling protections can be dynamically ingested from here...
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt = Backtesting(btconfig) ApiServer._bt = Backtesting(btconfig)
@ -327,6 +327,8 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
return { return {
"status": "running", "status": "running",
"running": True, "running": True,
"progress": 0,
"step": str(BacktestState.STARTUP),
"status_msg": "Backtest started", "status_msg": "Backtest started",
} }
@ -341,6 +343,8 @@ def api_get_backtest():
return { return {
"status": "running", "status": "running",
"running": True, "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", "status_msg": "Backtest running",
} }
@ -348,6 +352,8 @@ def api_get_backtest():
return { return {
"status": "not_started", "status": "not_started",
"running": False, "running": False,
"step": "",
"progress": 0,
"status_msg": "Backtesting not yet executed" "status_msg": "Backtesting not yet executed"
} }
@ -355,6 +361,8 @@ def api_get_backtest():
"status": "ended", "status": "ended",
"running": False, "running": False,
"status_msg": "Backtest ended", "status_msg": "Backtest ended",
"step": "finished",
"progress": 1,
"backtest_result": ApiServer._bt.results, "backtest_result": ApiServer._bt.results,
} }
@ -366,6 +374,7 @@ def api_delete_backtest():
return { return {
"status": "running", "status": "running",
"running": True, "running": True,
"progress": 0,
"status_msg": "Backtest running", "status_msg": "Backtest running",
} }
if ApiServer._bt: if ApiServer._bt:
@ -377,5 +386,6 @@ def api_delete_backtest():
return { return {
"status": "reset", "status": "reset",
"running": False, "running": False,
"progress": 0,
"status_msg": "Backtesting reset", "status_msg": "Backtesting reset",
} }