Use backtesting output for hyperopt results

This commit is contained in:
Matthias 2021-04-28 22:33:58 +02:00
parent 545cba7fd8
commit 6aaaad29d7
2 changed files with 61 additions and 48 deletions

View File

@ -8,7 +8,7 @@ import locale
import logging import logging
import random import random
import warnings import warnings
from datetime import datetime from datetime import datetime, timezone
from math import ceil from math import ceil
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
@ -30,6 +30,8 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.optimize.hyperopt_tools import HyperoptTools 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.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
from freqtrade.strategy import IStrategy from freqtrade.strategy import IStrategy
@ -79,8 +81,7 @@ class Hyperopt:
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
strategy = str(self.config['strategy']) strategy = str(self.config['strategy'])
self.results_file = (self.config['user_data_dir'] / self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' /
'hyperopt_results' /
f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') f'strategy_{strategy}_hyperopt_results_{time_now}.pickle')
self.data_pickle_file = (self.config['user_data_dir'] / self.data_pickle_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl') 'hyperopt_results' / 'hyperopt_tickerdata.pkl')
@ -246,6 +247,7 @@ class Hyperopt:
Used Optimize function. Called once per epoch to optimize whatever is configured. Used Optimize function. Called once per epoch to optimize whatever is configured.
Keep this function as optimized as possible! Keep this function as optimized as possible!
""" """
backtest_start_time = datetime.now(timezone.utc)
params_dict = self._get_params_dict(raw_params) params_dict = self._get_params_dict(raw_params)
params_details = self._get_params_details(params_dict) params_details = self._get_params_details(params_dict)
@ -284,19 +286,31 @@ class Hyperopt:
max_open_trades=self.max_open_trades, max_open_trades=self.max_open_trades,
position_stacking=self.position_stacking, position_stacking=self.position_stacking,
enable_protections=self.config.get('enable_protections', False), enable_protections=self.config.get('enable_protections', False),
) )
return self._get_results_dict(backtesting_results, min_date, max_date, 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']),
'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,
params_dict, params_details, params_dict, params_details,
processed=processed) processed=processed)
def _get_results_dict(self, backtesting_results, min_date, max_date, def _get_results_dict(self, backtesting_results, min_date, max_date,
params_dict, params_details, processed: Dict[str, DataFrame]): params_dict, params_details, processed: Dict[str, DataFrame]):
results_metrics = self._calculate_results_metrics(backtesting_results)
results_explanation = self._format_results_explanation_string(results_metrics)
trade_count = results_metrics['trade_count'] strat_stats = generate_strategy_stats(processed, '', backtesting_results,
total_profit = results_metrics['total_profit'] min_date, max_date, market_change=0)
results_explanation = self._format_results_explanation_string(strat_stats)
trade_count = strat_stats['total_trades']
total_profit = strat_stats['profit_total']
# If this evaluation contains too short amount of trades to be # If this evaluation contains too short amount of trades to be
# interesting -- consider it as 'bad' (assigned max. loss value) # interesting -- consider it as 'bad' (assigned max. loss value)
@ -304,48 +318,32 @@ class Hyperopt:
# path. We do not want to optimize 'hodl' strategies. # path. We do not want to optimize 'hodl' strategies.
loss: float = MAX_LOSS loss: float = MAX_LOSS
if trade_count >= self.config['hyperopt_min_trades']: if trade_count >= self.config['hyperopt_min_trades']:
loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count, loss = self.calculate_loss(results=backtesting_results['results'],
trade_count=trade_count,
min_date=min_date.datetime, max_date=max_date.datetime, min_date=min_date.datetime, max_date=max_date.datetime,
config=self.config, processed=processed) config=self.config, processed=processed)
return { return {
'loss': loss, 'loss': loss,
'params_dict': params_dict, 'params_dict': params_dict,
'params_details': params_details, 'params_details': params_details,
'results_metrics': results_metrics, 'results_metrics': strat_stats,
'results_explanation': results_explanation, 'results_explanation': results_explanation,
'total_profit': total_profit, 'total_profit': total_profit,
} }
def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict:
wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0])
draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0])
losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0])
return {
'trade_count': len(backtesting_results.index),
'wins': wins,
'draws': draws,
'losses': losses,
'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}",
'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0,
'median_profit': backtesting_results['profit_ratio'].median() * 100.0,
'total_profit': backtesting_results['profit_abs'].sum(),
'profit': backtesting_results['profit_ratio'].sum() * 100.0,
'duration': backtesting_results['trade_duration'].mean(),
}
def _format_results_explanation_string(self, results_metrics: Dict) -> str: def _format_results_explanation_string(self, results_metrics: Dict) -> str:
""" """
Return the formatted results explanation in a string Return the formatted results explanation in a string
""" """
stake_cur = self.config['stake_currency'] stake_cur = self.config['stake_currency']
return (f"{results_metrics['trade_count']:6d} trades. " return (f"{results_metrics['total_trades']:6d} trades. "
f"{results_metrics['wins']}/{results_metrics['draws']}" f"{results_metrics['wins']}/{results_metrics['draws']}"
f"/{results_metrics['losses']} Wins/Draws/Losses. " f"/{results_metrics['losses']} Wins/Draws/Losses. "
f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. "
f"Median profit {results_metrics['median_profit']: 6.2f}%. " f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. "
f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_cur} "
f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). "
f"Avg duration {results_metrics['duration']:5.1f} min." f"Avg duration {results_metrics['holding_avg']} min."
).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8')
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:

View File

@ -12,7 +12,7 @@ from colorama import Fore, Style
from pandas import isna, json_normalize from pandas import isna, json_normalize
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_dict from freqtrade.misc import round_coin_value, round_dict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -169,11 +169,24 @@ class HyperoptTools():
# Ensure compatibility with older versions of hyperopt results # Ensure compatibility with older versions of hyperopt results
trials['results_metrics.winsdrawslosses'] = 'N/A' trials['results_metrics.winsdrawslosses'] = 'N/A'
if 'results_metrics.total_trades' in trials:
# New mode, using backtest result for metrics
trials['results_metrics.winsdrawslosses'] = trials.apply(
lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} "
f"{x['results_metrics.losses']:>4}", axis=1)
trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades',
'results_metrics.winsdrawslosses',
'results_metrics.profit_mean', 'results_metrics.profit_total_abs',
'results_metrics.profit_total', 'results_metrics.holding_avg',
'loss', 'is_initial_point', 'is_best']]
else:
# Legacy mode
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.winsdrawslosses', 'results_metrics.winsdrawslosses',
'results_metrics.avg_profit', 'results_metrics.total_profit', 'results_metrics.avg_profit', 'results_metrics.total_profit',
'results_metrics.profit', 'results_metrics.duration', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']] 'loss', 'is_initial_point', 'is_best']]
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
'Total profit', 'Profit', 'Avg duration', 'Objective', 'Total profit', 'Profit', 'Avg duration', 'Objective',
'is_initial_point', 'is_best'] 'is_initial_point', 'is_best']
@ -188,21 +201,23 @@ class HyperoptTools():
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
) )
trials['Avg profit'] = trials['Avg profit'].apply( trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') lambda x: f'{x:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
) )
trials['Avg duration'] = trials['Avg duration'].apply( trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}"
if not isna(x) else "--".rjust(7, ' ')
) )
trials['Objective'] = trials['Objective'].apply( trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
) )
stake_currency = config['stake_currency']
trials['Profit'] = trials.apply( trials['Profit'] = trials.apply(
lambda x: '{:,.8f} {} {}'.format( lambda x: '{} {}'.format(
x['Total profit'], config['stake_currency'], round_coin_value(x['Total profit'], stake_currency),
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
).rjust(25+len(config['stake_currency'])) ).rjust(25+len(stake_currency))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),
axis=1 axis=1
) )
trials = trials.drop(columns=['Total profit']) trials = trials.drop(columns=['Total profit'])