Merge branch 'develop' into pr/yazeed/3008
This commit is contained in:
@@ -18,8 +18,10 @@ from freqtrade.data.converter import trim_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.optimize.optimize_reports import (show_backtest_results,
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats,
|
||||
show_backtest_results,
|
||||
store_backtest_result)
|
||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.state import RunMode
|
||||
@@ -63,11 +65,6 @@ class Backtesting:
|
||||
self.strategylist: List[IStrategy] = []
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
|
||||
if config.get('fee'):
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0])
|
||||
|
||||
if self.config.get('runmode') != RunMode.HYPEROPT:
|
||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||
IStrategy.dp = self.dataprovider
|
||||
@@ -84,12 +81,31 @@ class Backtesting:
|
||||
self.strategylist.append(StrategyResolver.load_strategy(self.config))
|
||||
validate_config_consistency(self.config)
|
||||
|
||||
if "ticker_interval" not in self.config:
|
||||
if "timeframe" not in self.config:
|
||||
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
|
||||
"configuration or as cli argument `--ticker-interval 5m`")
|
||||
self.timeframe = str(self.config.get('ticker_interval'))
|
||||
"configuration or as cli argument `--timeframe 5m`")
|
||||
self.timeframe = str(self.config.get('timeframe'))
|
||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||
|
||||
if len(self.strategylist) > 1 and 'PrecisionFilter' in self.pairlists.name_list:
|
||||
raise OperationalException(
|
||||
"PrecisionFilter not allowed for backtesting multiple strategies."
|
||||
)
|
||||
|
||||
self.pairlists.refresh_pairlist()
|
||||
|
||||
if len(self.pairlists.whitelist) == 0:
|
||||
raise OperationalException("No pair in whitelist.")
|
||||
|
||||
if config.get('fee', None) is not None:
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||
|
||||
# Get maximum required startup period
|
||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||
# Load one (first) strategy
|
||||
@@ -111,7 +127,7 @@ class Backtesting:
|
||||
|
||||
data = history.load_data(
|
||||
datadir=self.config['datadir'],
|
||||
pairs=self.config['exchange']['pair_whitelist'],
|
||||
pairs=self.pairlists.whitelist,
|
||||
timeframe=self.timeframe,
|
||||
timerange=timerange,
|
||||
startup_candles=self.required_startup,
|
||||
@@ -401,4 +417,5 @@ class Backtesting:
|
||||
if self.config.get('export', False):
|
||||
store_backtest_result(self.config['exportfilename'], all_results)
|
||||
# Show backtest results
|
||||
show_backtest_results(self.config, data, all_results)
|
||||
stats = generate_backtest_stats(self.config, data, all_results)
|
||||
show_backtest_results(self.config, stats)
|
||||
|
@@ -42,8 +42,8 @@ class DefaultHyperOptLoss(IHyperOptLoss):
|
||||
* 0.25: Avoiding trade loss
|
||||
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
||||
"""
|
||||
total_profit = results.profit_percent.sum()
|
||||
trade_duration = results.trade_duration.mean()
|
||||
total_profit = results['profit_percent'].sum()
|
||||
trade_duration = results['trade_duration'].mean()
|
||||
|
||||
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||
|
@@ -12,7 +12,7 @@ from math import ceil
|
||||
from collections import OrderedDict
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
from pprint import pformat
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import rapidjson
|
||||
@@ -49,9 +49,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
INITIAL_POINTS = 30
|
||||
|
||||
# Keep no more than 2*SKOPT_MODELS_MAX_NUM models
|
||||
# in the skopt models list
|
||||
SKOPT_MODELS_MAX_NUM = 10
|
||||
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||
# in the skopt model queue, to optimize memory consumption
|
||||
SKOPT_MODEL_QUEUE_SIZE = 10
|
||||
|
||||
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
||||
|
||||
@@ -75,8 +75,8 @@ class Hyperopt:
|
||||
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
|
||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||
|
||||
self.trials_file = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||
self.results_file = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||
self.data_pickle_file = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
|
||||
self.total_epochs = config.get('epochs', 0)
|
||||
@@ -88,10 +88,10 @@ class Hyperopt:
|
||||
else:
|
||||
logger.info("Continuing on previous hyperopt results.")
|
||||
|
||||
self.num_trials_saved = 0
|
||||
self.num_epochs_saved = 0
|
||||
|
||||
# Previous evaluations
|
||||
self.trials: List = []
|
||||
self.epochs: List = []
|
||||
|
||||
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
||||
@@ -132,7 +132,7 @@ class Hyperopt:
|
||||
"""
|
||||
Remove hyperopt pickle files to restart hyperopt.
|
||||
"""
|
||||
for f in [self.data_pickle_file, self.trials_file]:
|
||||
for f in [self.data_pickle_file, self.results_file]:
|
||||
p = Path(f)
|
||||
if p.is_file():
|
||||
logger.info(f"Removing `{p}`.")
|
||||
@@ -151,27 +151,26 @@ class Hyperopt:
|
||||
# and the values are taken from the list of parameters.
|
||||
return {d.name: v for d, v in zip(dimensions, raw_params)}
|
||||
|
||||
def save_trials(self, final: bool = False) -> None:
|
||||
def _save_results(self) -> None:
|
||||
"""
|
||||
Save hyperopt trials to file
|
||||
Save hyperopt results to file
|
||||
"""
|
||||
num_trials = len(self.trials)
|
||||
if num_trials > self.num_trials_saved:
|
||||
logger.debug(f"Saving {num_trials} {plural(num_trials, 'epoch')}.")
|
||||
dump(self.trials, self.trials_file)
|
||||
self.num_trials_saved = num_trials
|
||||
if final:
|
||||
logger.info(f"{num_trials} {plural(num_trials, 'epoch')} "
|
||||
f"saved to '{self.trials_file}'.")
|
||||
num_epochs = len(self.epochs)
|
||||
if num_epochs > self.num_epochs_saved:
|
||||
logger.debug(f"Saving {num_epochs} {plural(num_epochs, 'epoch')}.")
|
||||
dump(self.epochs, self.results_file)
|
||||
self.num_epochs_saved = num_epochs
|
||||
logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
|
||||
f"saved to '{self.results_file}'.")
|
||||
|
||||
@staticmethod
|
||||
def _read_trials(trials_file: Path) -> List:
|
||||
def _read_results(results_file: Path) -> List:
|
||||
"""
|
||||
Read hyperopt trials file
|
||||
Read hyperopt results from file
|
||||
"""
|
||||
logger.info("Reading Trials from '%s'", trials_file)
|
||||
trials = load(trials_file)
|
||||
return trials
|
||||
logger.info("Reading epochs from '%s'", results_file)
|
||||
data = load(results_file)
|
||||
return data
|
||||
|
||||
def _get_params_details(self, params: Dict) -> Dict:
|
||||
"""
|
||||
@@ -231,6 +230,9 @@ class Hyperopt:
|
||||
if space in ['buy', 'sell']:
|
||||
result_dict.setdefault('params', {}).update(space_params)
|
||||
elif space == 'roi':
|
||||
# TODO: get rid of OrderedDict when support for python 3.6 will be
|
||||
# dropped (dicts keep the order as the language feature)
|
||||
|
||||
# Convert keys in min_roi dict to strings because
|
||||
# rapidjson cannot dump dicts with integer keys...
|
||||
# OrderedDict is used to keep the numeric order of the items
|
||||
@@ -245,11 +247,24 @@ class Hyperopt:
|
||||
def _params_pretty_print(params, space: str, header: str) -> None:
|
||||
if space in params:
|
||||
space_params = Hyperopt._space_params(params, space, 5)
|
||||
params_result = f"\n# {header}\n"
|
||||
if space == 'stoploss':
|
||||
print(header, space_params.get('stoploss'))
|
||||
params_result += f"stoploss = {space_params.get('stoploss')}"
|
||||
elif space == 'roi':
|
||||
# TODO: get rid of OrderedDict when support for python 3.6 will be
|
||||
# dropped (dicts keep the order as the language feature)
|
||||
minimal_roi_result = rapidjson.dumps(
|
||||
OrderedDict(
|
||||
(str(k), v) for k, v in space_params.items()
|
||||
),
|
||||
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||
params_result += f"minimal_roi = {minimal_roi_result}"
|
||||
else:
|
||||
print(header)
|
||||
pprint(space_params, indent=4)
|
||||
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
|
||||
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
|
||||
|
||||
params_result = params_result.replace("\n", "\n ")
|
||||
print(params_result)
|
||||
|
||||
@staticmethod
|
||||
def _space_params(params, space: str, r: int = None) -> Dict:
|
||||
@@ -304,8 +319,9 @@ class Hyperopt:
|
||||
trials.columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit',
|
||||
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
|
||||
trials['is_profit'] = False
|
||||
trials.loc[trials['is_initial_point'], 'Best'] = '*'
|
||||
trials.loc[trials['is_initial_point'], 'Best'] = '* '
|
||||
trials.loc[trials['is_best'], 'Best'] = 'Best'
|
||||
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
|
||||
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
|
||||
trials['Trades'] = trials['Trades'].astype(str)
|
||||
|
||||
@@ -375,27 +391,35 @@ class Hyperopt:
|
||||
|
||||
# Verification for overwrite
|
||||
if path.isfile(csv_file):
|
||||
logger.error("CSV-File already exists!")
|
||||
logger.error(f"CSV file already exists: {csv_file}")
|
||||
return
|
||||
|
||||
try:
|
||||
io.open(csv_file, 'w+').close()
|
||||
except IOError:
|
||||
logger.error("Filed to create CSV-File!")
|
||||
logger.error(f"Failed to create CSV file: {csv_file}")
|
||||
return
|
||||
|
||||
trials = json_normalize(results, max_level=1)
|
||||
trials['Best'] = ''
|
||||
trials['Stake currency'] = config['stake_currency']
|
||||
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.avg_profit', 'results_metrics.total_profit',
|
||||
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
|
||||
'loss', 'is_initial_point', 'is_best']]
|
||||
trials.columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency',
|
||||
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
|
||||
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.avg_profit', 'results_metrics.total_profit',
|
||||
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
|
||||
trials = trials[base_metrics + param_metrics]
|
||||
|
||||
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency',
|
||||
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
|
||||
param_columns = list(results[0]['params_dict'].keys())
|
||||
trials.columns = base_columns + param_columns
|
||||
|
||||
trials['is_profit'] = False
|
||||
trials.loc[trials['is_initial_point'], 'Best'] = '*'
|
||||
trials.loc[trials['is_best'], 'Best'] = 'Best'
|
||||
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
|
||||
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
|
||||
trials['Epoch'] = trials['Epoch'].astype(str)
|
||||
trials['Trades'] = trials['Trades'].astype(str)
|
||||
@@ -418,7 +442,7 @@ class Hyperopt:
|
||||
|
||||
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
|
||||
trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8')
|
||||
print("CSV-File created!")
|
||||
logger.info(f"CSV file created: {csv_file}")
|
||||
|
||||
def has_space(self, space: str) -> bool:
|
||||
"""
|
||||
@@ -570,43 +594,28 @@ class Hyperopt:
|
||||
n_initial_points=INITIAL_POINTS,
|
||||
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
||||
random_state=self.random_state,
|
||||
model_queue_size=SKOPT_MODEL_QUEUE_SIZE,
|
||||
)
|
||||
|
||||
def fix_optimizer_models_list(self) -> None:
|
||||
"""
|
||||
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
|
||||
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
|
||||
|
||||
This may cease working when skopt updates if implementation of this intrinsic
|
||||
part changes.
|
||||
"""
|
||||
n = len(self.opt.models) - SKOPT_MODELS_MAX_NUM
|
||||
# Keep no more than 2*SKOPT_MODELS_MAX_NUM models in the skopt models list,
|
||||
# remove the old ones. These are actually of no use, the current model
|
||||
# from the estimator is the only one used in the skopt optimizer.
|
||||
# Freqtrade code also does not inspect details of the models.
|
||||
if n >= SKOPT_MODELS_MAX_NUM:
|
||||
logger.debug(f"Fixing skopt models list, removing {n} old items...")
|
||||
del self.opt.models[0:n]
|
||||
|
||||
def run_optimizer_parallel(self, parallel, asked, i) -> List:
|
||||
return parallel(delayed(
|
||||
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
|
||||
|
||||
@staticmethod
|
||||
def load_previous_results(trials_file: Path) -> List:
|
||||
def load_previous_results(results_file: Path) -> List:
|
||||
"""
|
||||
Load data for epochs from the file if we have one
|
||||
"""
|
||||
trials: List = []
|
||||
if trials_file.is_file() and trials_file.stat().st_size > 0:
|
||||
trials = Hyperopt._read_trials(trials_file)
|
||||
if trials[0].get('is_best') is None:
|
||||
epochs: List = []
|
||||
if results_file.is_file() and results_file.stat().st_size > 0:
|
||||
epochs = Hyperopt._read_results(results_file)
|
||||
# Detection of some old format, without 'is_best' field saved
|
||||
if epochs[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
"The file with Hyperopt results is incompatible with this version "
|
||||
"of Freqtrade and cannot be loaded.")
|
||||
logger.info(f"Loaded {len(trials)} previous evaluations from disk.")
|
||||
return trials
|
||||
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
|
||||
return epochs
|
||||
|
||||
def _set_random_state(self, random_state: Optional[int]) -> int:
|
||||
return random_state or random.randint(1, 2**16 - 1)
|
||||
@@ -632,8 +641,9 @@ class Hyperopt:
|
||||
|
||||
# We don't need exchange instance anymore while running hyperopt
|
||||
self.backtesting.exchange = None # type: ignore
|
||||
self.backtesting.pairlists = None # type: ignore
|
||||
|
||||
self.trials = self.load_previous_results(self.trials_file)
|
||||
self.epochs = self.load_previous_results(self.results_file)
|
||||
|
||||
cpus = cpu_count()
|
||||
logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
|
||||
@@ -669,7 +679,7 @@ class Hyperopt:
|
||||
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
|
||||
]
|
||||
with progressbar.ProgressBar(
|
||||
maxval=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
|
||||
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
|
||||
widgets=widgets
|
||||
) as pbar:
|
||||
EVALS = ceil(self.total_epochs / jobs)
|
||||
@@ -682,7 +692,6 @@ class Hyperopt:
|
||||
asked = self.opt.ask(n_points=current_jobs)
|
||||
f_val = self.run_optimizer_parallel(parallel, asked, i)
|
||||
self.opt.tell(asked, [v['loss'] for v in f_val])
|
||||
self.fix_optimizer_models_list()
|
||||
|
||||
# Calculate progressbar outputs
|
||||
for j, val in enumerate(f_val):
|
||||
@@ -703,23 +712,25 @@ class Hyperopt:
|
||||
|
||||
if is_best:
|
||||
self.current_best_loss = val['loss']
|
||||
self.trials.append(val)
|
||||
self.epochs.append(val)
|
||||
|
||||
# Save results after each best epoch and every 100 epochs
|
||||
if is_best or current % 100 == 0:
|
||||
self.save_trials()
|
||||
self._save_results()
|
||||
|
||||
pbar.update(current)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print('User interrupted..')
|
||||
|
||||
self.save_trials(final=True)
|
||||
self._save_results()
|
||||
logger.info(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
|
||||
f"saved to '{self.results_file}'.")
|
||||
|
||||
if self.trials:
|
||||
sorted_trials = sorted(self.trials, key=itemgetter('loss'))
|
||||
results = sorted_trials[0]
|
||||
self.print_epoch_details(results, self.total_epochs, self.print_json)
|
||||
if self.epochs:
|
||||
sorted_epochs = sorted(self.epochs, key=itemgetter('loss'))
|
||||
best_epoch = sorted_epochs[0]
|
||||
self.print_epoch_details(best_epoch, self.total_epochs, self.print_json)
|
||||
else:
|
||||
# This is printed when Ctrl+C is pressed quickly, before first epochs have
|
||||
# a chance to be evaluated.
|
||||
|
@@ -31,13 +31,15 @@ class IHyperOpt(ABC):
|
||||
Class attributes you can use:
|
||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
||||
"""
|
||||
ticker_interval: str
|
||||
ticker_interval: str # DEPRECATED
|
||||
timeframe: str
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
self.config = config
|
||||
|
||||
# Assign ticker_interval to be used in hyperopt
|
||||
IHyperOpt.ticker_interval = str(config['ticker_interval'])
|
||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||
IHyperOpt.timeframe = str(config['timeframe'])
|
||||
|
||||
@staticmethod
|
||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
@@ -218,9 +220,10 @@ class IHyperOpt(ABC):
|
||||
# Why do I still need such shamanic mantras in modern python?
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
state['ticker_interval'] = self.ticker_interval
|
||||
state['timeframe'] = self.timeframe
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
IHyperOpt.ticker_interval = state['ticker_interval']
|
||||
IHyperOpt.ticker_interval = state['timeframe']
|
||||
IHyperOpt.timeframe = state['timeframe']
|
||||
|
@@ -14,7 +14,7 @@ class IHyperOptLoss(ABC):
|
||||
Interface for freqtrade hyperopt Loss functions.
|
||||
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
|
||||
"""
|
||||
ticker_interval: str
|
||||
timeframe: str
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
|
@@ -34,5 +34,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Objective function, returns smaller number for better results.
|
||||
"""
|
||||
total_profit = results.profit_percent.sum()
|
||||
total_profit = results['profit_percent'].sum()
|
||||
return 1 - total_profit / EXPECTED_MAX_PROFIT
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pandas import DataFrame
|
||||
from tabulate import tabulate
|
||||
@@ -18,10 +18,7 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame
|
||||
:param all_results: Dict of Dataframes, one results dataframe per strategy
|
||||
"""
|
||||
for strategy, results in all_results.items():
|
||||
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
|
||||
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
|
||||
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
|
||||
for index, t in results.iterrows()]
|
||||
records = backtest_result_to_list(results)
|
||||
|
||||
if records:
|
||||
filename = recordfilename
|
||||
@@ -34,153 +31,141 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame
|
||||
file_dump_json(filename, records)
|
||||
|
||||
|
||||
def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int,
|
||||
results: DataFrame, skip_nan: bool = False) -> str:
|
||||
def backtest_result_to_list(results: DataFrame) -> List[List]:
|
||||
"""
|
||||
Generates and returns a text table for the given backtest data and the results dataframe
|
||||
Converts a list of Backtest-results to list
|
||||
:param results: Dataframe containing results for one strategy
|
||||
:return: List of Lists containing the trades
|
||||
"""
|
||||
return [[t.pair, t.profit_percent, t.open_time.timestamp(),
|
||||
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
|
||||
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value]
|
||||
for index, t in results.iterrows()]
|
||||
|
||||
|
||||
def _get_line_floatfmt() -> List[str]:
|
||||
"""
|
||||
Generate floatformat (goes in line with _generate_result_line())
|
||||
"""
|
||||
return ['s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd']
|
||||
|
||||
|
||||
def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
|
||||
"""
|
||||
Generate header lines (goes in line with _generate_result_line())
|
||||
"""
|
||||
return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Wins', 'Draws', 'Losses']
|
||||
|
||||
|
||||
def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict:
|
||||
"""
|
||||
Generate one result dict, with "first_column" as key.
|
||||
"""
|
||||
return {
|
||||
'key': first_column,
|
||||
'trades': len(result),
|
||||
'profit_mean': result['profit_percent'].mean(),
|
||||
'profit_mean_pct': result['profit_percent'].mean() * 100.0,
|
||||
'profit_sum': result['profit_percent'].sum(),
|
||||
'profit_sum_pct': result['profit_percent'].sum() * 100.0,
|
||||
'profit_total_abs': result['profit_abs'].sum(),
|
||||
'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades,
|
||||
'duration_avg': str(timedelta(
|
||||
minutes=round(result['trade_duration'].mean()))
|
||||
) if not result.empty else '0:00',
|
||||
# 'duration_max': str(timedelta(
|
||||
# minutes=round(result['trade_duration'].max()))
|
||||
# ) if not result.empty else '0:00',
|
||||
# 'duration_min': str(timedelta(
|
||||
# minutes=round(result['trade_duration'].min()))
|
||||
# ) if not result.empty else '0:00',
|
||||
'wins': len(result[result['profit_abs'] > 0]),
|
||||
'draws': len(result[result['profit_abs'] == 0]),
|
||||
'losses': len(result[result['profit_abs'] < 0]),
|
||||
}
|
||||
|
||||
|
||||
def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int,
|
||||
results: DataFrame, skip_nan: bool = False) -> List[Dict]:
|
||||
"""
|
||||
Generates and returns a list for the given backtest data and the results dataframe
|
||||
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:param max_open_trades: Maximum allowed open trades
|
||||
:param results: Dataframe containing the backtest results
|
||||
:param skip_nan: Print "left open" open trades
|
||||
:return: pretty printed table with tabulate as string
|
||||
:return: List of Dicts containing the metrics per pair
|
||||
"""
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = [
|
||||
'Pair',
|
||||
'Buys',
|
||||
'Avg Profit %',
|
||||
'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}',
|
||||
'Tot Profit %',
|
||||
'Avg Duration',
|
||||
'Wins',
|
||||
'Draws',
|
||||
'Losses'
|
||||
]
|
||||
|
||||
for pair in data:
|
||||
result = results[results.pair == pair]
|
||||
if skip_nan and result.profit_abs.isnull().all():
|
||||
result = results[results['pair'] == pair]
|
||||
if skip_nan and result['profit_abs'].isnull().all():
|
||||
continue
|
||||
|
||||
tabular_data.append([
|
||||
pair,
|
||||
len(result.index),
|
||||
result.profit_percent.mean() * 100.0,
|
||||
result.profit_percent.sum() * 100.0,
|
||||
result.profit_abs.sum(),
|
||||
result.profit_percent.sum() * 100.0 / max_open_trades,
|
||||
str(timedelta(
|
||||
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
|
||||
len(result[result.profit_abs > 0]),
|
||||
len(result[result.profit_abs == 0]),
|
||||
len(result[result.profit_abs < 0])
|
||||
])
|
||||
tabular_data.append(_generate_result_line(result, max_open_trades, pair))
|
||||
|
||||
# Append Total
|
||||
tabular_data.append([
|
||||
'TOTAL',
|
||||
len(results.index),
|
||||
results.profit_percent.mean() * 100.0,
|
||||
results.profit_percent.sum() * 100.0,
|
||||
results.profit_abs.sum(),
|
||||
results.profit_percent.sum() * 100.0 / max_open_trades,
|
||||
str(timedelta(
|
||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||
len(results[results.profit_abs > 0]),
|
||||
len(results[results.profit_abs == 0]),
|
||||
len(results[results.profit_abs < 0])
|
||||
])
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(tabular_data, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
||||
tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL'))
|
||||
return tabular_data
|
||||
|
||||
|
||||
def generate_text_table_sell_reason(stake_currency: str, max_open_trades: int,
|
||||
results: DataFrame) -> str:
|
||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
:param stake_currency: Stakecurrency used
|
||||
:param max_open_trades: Max_open_trades parameter
|
||||
:param results: Dataframe containing the backtest results
|
||||
:return: pretty printed table with tabulate as string
|
||||
:param results: Dataframe containing the backtest result for one strategy
|
||||
:return: List of Dicts containing the metrics per Sell reason
|
||||
"""
|
||||
tabular_data = []
|
||||
headers = [
|
||||
"Sell Reason",
|
||||
"Sells",
|
||||
"Wins",
|
||||
"Draws",
|
||||
"Losses",
|
||||
"Avg Profit %",
|
||||
"Cum Profit %",
|
||||
f"Tot Profit {stake_currency}",
|
||||
"Tot Profit %",
|
||||
]
|
||||
|
||||
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||
result = results.loc[results['sell_reason'] == reason]
|
||||
wins = len(result[result['profit_abs'] > 0])
|
||||
draws = len(result[result['profit_abs'] == 0])
|
||||
loss = len(result[result['profit_abs'] < 0])
|
||||
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
|
||||
profit_sum = round(result["profit_percent"].sum() * 100.0, 2)
|
||||
profit_tot = result['profit_abs'].sum()
|
||||
|
||||
profit_mean = result['profit_percent'].mean()
|
||||
profit_sum = result["profit_percent"].sum()
|
||||
profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2)
|
||||
|
||||
tabular_data.append(
|
||||
[
|
||||
reason.value,
|
||||
count,
|
||||
wins,
|
||||
draws,
|
||||
loss,
|
||||
profit_mean,
|
||||
profit_sum,
|
||||
profit_tot,
|
||||
profit_percent_tot,
|
||||
]
|
||||
{
|
||||
'sell_reason': reason.value,
|
||||
'trades': count,
|
||||
'wins': len(result[result['profit_abs'] > 0]),
|
||||
'draws': len(result[result['profit_abs'] == 0]),
|
||||
'losses': len(result[result['profit_abs'] < 0]),
|
||||
'profit_mean': profit_mean,
|
||||
'profit_mean_pct': round(profit_mean * 100, 2),
|
||||
'profit_sum': profit_sum,
|
||||
'profit_sum_pct': round(profit_sum * 100, 2),
|
||||
'profit_total_abs': result['profit_abs'].sum(),
|
||||
'profit_pct_total': profit_percent_tot,
|
||||
}
|
||||
)
|
||||
return tabulate(tabular_data, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||
return tabular_data
|
||||
|
||||
|
||||
def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
|
||||
all_results: Dict) -> str:
|
||||
def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
|
||||
all_results: Dict) -> List[Dict]:
|
||||
"""
|
||||
Generate summary table per strategy
|
||||
Generate summary per strategy
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:param max_open_trades: Maximum allowed open trades used for backtest
|
||||
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
|
||||
:return: pretty printed table with tabulate as string
|
||||
:return: List of Dicts containing the metrics per Strategy
|
||||
"""
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = ['Strategy', 'Buys', 'Avg Profit %', 'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Wins', 'Draws', 'Losses']
|
||||
for strategy, results in all_results.items():
|
||||
tabular_data.append([
|
||||
strategy,
|
||||
len(results.index),
|
||||
results.profit_percent.mean() * 100.0,
|
||||
results.profit_percent.sum() * 100.0,
|
||||
results.profit_abs.sum(),
|
||||
results.profit_percent.sum() * 100.0 / max_open_trades,
|
||||
str(timedelta(
|
||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||
len(results[results.profit_abs > 0]),
|
||||
len(results[results.profit_abs == 0]),
|
||||
len(results[results.profit_abs < 0])
|
||||
])
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(tabular_data, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
||||
tabular_data.append(_generate_result_line(results, max_open_trades, strategy))
|
||||
return tabular_data
|
||||
|
||||
|
||||
def generate_edge_table(results: dict) -> str:
|
||||
|
||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')
|
||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
|
||||
tabular_data = []
|
||||
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
|
||||
'Required Risk Reward', 'Expectancy', 'Total Number of Trades',
|
||||
@@ -204,40 +189,145 @@ def generate_edge_table(results: dict) -> str:
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
||||
|
||||
|
||||
def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
|
||||
all_results: Dict[str, DataFrame]):
|
||||
def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame],
|
||||
all_results: Dict[str, DataFrame]) -> Dict[str, Any]:
|
||||
"""
|
||||
:param config: Configuration object used for backtest
|
||||
:param btdata: Backtest data
|
||||
:param all_results: backtest result - dictionary with { Strategy: results}.
|
||||
:return:
|
||||
Dictionary containing results per strategy and a stratgy summary.
|
||||
"""
|
||||
stake_currency = config['stake_currency']
|
||||
max_open_trades = config['max_open_trades']
|
||||
result: Dict[str, Any] = {'strategy': {}}
|
||||
for strategy, results in all_results.items():
|
||||
|
||||
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||
max_open_trades=max_open_trades,
|
||||
results=results, skip_nan=False)
|
||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||
results=results)
|
||||
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||
max_open_trades=max_open_trades,
|
||||
results=results.loc[results['open_at_end']],
|
||||
skip_nan=True)
|
||||
strat_stats = {
|
||||
'trades': backtest_result_to_list(results),
|
||||
'results_per_pair': pair_results,
|
||||
'sell_reason_summary': sell_reason_stats,
|
||||
'left_open_trades': left_open_results,
|
||||
}
|
||||
result['strategy'][strategy] = strat_stats
|
||||
|
||||
strategy_results = generate_strategy_metrics(stake_currency=stake_currency,
|
||||
max_open_trades=max_open_trades,
|
||||
all_results=all_results)
|
||||
|
||||
result['strategy_comparison'] = strategy_results
|
||||
|
||||
return result
|
||||
|
||||
|
||||
###
|
||||
# Start output section
|
||||
###
|
||||
|
||||
def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
"""
|
||||
Generates and returns a text table for the given backtest data and the results dataframe
|
||||
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
|
||||
headers = _get_line_header('Pair', stake_currency)
|
||||
floatfmt = _get_line_floatfmt()
|
||||
output = [[
|
||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
||||
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
||||
] for t in pair_results]
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(output, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
:param sell_reason_stats: Sell reason metrics
|
||||
:param stake_currency: Stakecurrency used
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
headers = [
|
||||
'Sell Reason',
|
||||
'Sells',
|
||||
'Wins',
|
||||
'Draws',
|
||||
'Losses',
|
||||
'Avg Profit %',
|
||||
'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}',
|
||||
'Tot Profit %',
|
||||
]
|
||||
|
||||
output = [[
|
||||
t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'],
|
||||
t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'],
|
||||
] for t in sell_reason_stats]
|
||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
||||
"""
|
||||
Generate summary table per strategy
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:param max_open_trades: Maximum allowed open trades used for backtest
|
||||
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
floatfmt = _get_line_floatfmt()
|
||||
headers = _get_line_header('Strategy', stake_currency)
|
||||
|
||||
output = [[
|
||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
||||
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
||||
] for t in strategy_results]
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(output, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def show_backtest_results(config: Dict, backtest_stats: Dict):
|
||||
stake_currency = config['stake_currency']
|
||||
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
|
||||
# Print results
|
||||
print(f"Result for strategy {strategy}")
|
||||
table = generate_text_table(btdata, stake_currency=config['stake_currency'],
|
||||
max_open_trades=config['max_open_trades'],
|
||||
results=results)
|
||||
table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
|
||||
if isinstance(table, str):
|
||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = generate_text_table_sell_reason(stake_currency=config['stake_currency'],
|
||||
max_open_trades=config['max_open_trades'],
|
||||
results=results)
|
||||
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
||||
stake_currency=stake_currency)
|
||||
if isinstance(table, str):
|
||||
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = generate_text_table(btdata,
|
||||
stake_currency=config['stake_currency'],
|
||||
max_open_trades=config['max_open_trades'],
|
||||
results=results.loc[results.open_at_end], skip_nan=True)
|
||||
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
||||
if isinstance(table, str):
|
||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
if isinstance(table, str):
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
print()
|
||||
if len(all_results) > 1:
|
||||
|
||||
if len(backtest_stats['strategy']) > 1:
|
||||
# Print Strategy summary table
|
||||
table = generate_text_table_strategy(config['stake_currency'],
|
||||
config['max_open_trades'],
|
||||
all_results=all_results)
|
||||
|
||||
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
||||
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
|
Reference in New Issue
Block a user