Properly track bt progress ...

This commit is contained in:
Matthias 2021-03-21 15:56:36 +01:00
parent 03140a0ecb
commit 134c61126e
4 changed files with 53 additions and 28 deletions

View File

@ -8,7 +8,8 @@ class BacktestState(Enum):
STARTUP = 1 STARTUP = 1
DATALOAD = 2 DATALOAD = 2
ANALYZE = 3 ANALYZE = 3
BACKTEST = 4 CONVERT = 4
BACKTEST = 5
def __str__(self): def __str__(self):
return f"{self.name.lower()}" return f"{self.name.lower()}"

View File

@ -21,6 +21,7 @@ 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
from freqtrade.optimize.bt_progress import BTProgress
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
store_backtest_stats) store_backtest_stats)
from freqtrade.persistence import LocalTrade, PairLocks, Trade from freqtrade.persistence import LocalTrade, PairLocks, Trade
@ -118,26 +119,13 @@ 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.progress = BTProgress()
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
@ -160,7 +148,7 @@ 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) self.progress.init_step(BacktestState.DATALOAD, 1)
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')))
@ -185,7 +173,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) self.progress.set_new_value(1)
return data, timerange return data, timerange
def prepare_backtest(self, enable_protections): def prepare_backtest(self, enable_protections):
@ -210,8 +198,11 @@ class Backtesting:
# and eventually change the constants for indexes at the top # and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
data: Dict = {} data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed))
# Create dict with data # Create dict with data
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
self.progress.increment()
if not pair_data.empty: if not pair_data.empty:
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_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_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
open_trade_count = 0 open_trade_count = 0
self.set_progress(BacktestState.BACKTEST, 0) self.progress.init_step(BacktestState.BACKTEST, int(
self._max_steps = int((end_date - start_date) / timedelta(minutes=self.timeframe_min)) (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:
@ -484,7 +475,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 self.progress.increment()
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)
@ -500,8 +491,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.progress.init_step(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)
@ -528,7 +519,6 @@ 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)} '

View File

@ -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)

View File

@ -291,6 +291,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
if (not ApiServer._bt if (not ApiServer._bt
or lastconfig.get('timeframe') != strat.timeframe 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('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)):
@ -298,11 +299,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt = Backtesting(btconfig) ApiServer._bt = Backtesting(btconfig)
# Reset data if backtesting is reloaded # Reset data if backtesting is reloaded
# TODO: is this always necessary??
ApiServer._backtestdata = None
if (not ApiServer._backtestdata or not ApiServer._bt_timerange 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['timerange'] = btconfig['timerange']
lastconfig['protections'] = btconfig.get('protections', []) lastconfig['protections'] = btconfig.get('protections', [])
lastconfig['enable_protections'] = btconfig.get('enable_protections') lastconfig['enable_protections'] = btconfig.get('enable_protections')
@ -344,8 +344,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), "step": ApiServer._bt.progress.action if ApiServer._bt else str(BacktestState.STARTUP),
"progress": ApiServer._bt.get_progress() if ApiServer._bt else 0, "progress": ApiServer._bt.progress.progress if ApiServer._bt else 0,
"trade_count": len(LocalTrade.trades), "trade_count": len(LocalTrade.trades),
"status_msg": "Backtest running", "status_msg": "Backtest running",
} }