mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-12-02 10:03:05 +00:00
Merge branch 'develop' into pr/Antreasgr/4838
This commit is contained in:
@@ -3,17 +3,15 @@ import io
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import rapidjson
|
||||
import tabulate
|
||||
from colorama import Fore, Style
|
||||
from joblib import load
|
||||
from pandas import isna, json_normalize
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import round_dict
|
||||
from freqtrade.misc import round_coin_value, round_dict
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,13 +19,38 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class HyperoptTools():
|
||||
|
||||
@staticmethod
|
||||
def has_space(config: Dict[str, Any], space: str) -> bool:
|
||||
"""
|
||||
Tell if the space value is contained in the configuration
|
||||
"""
|
||||
# The 'trailing' space is not included in the 'default' set of spaces
|
||||
if space == 'trailing':
|
||||
return any(s in config['spaces'] for s in [space, 'all'])
|
||||
else:
|
||||
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
||||
|
||||
@staticmethod
|
||||
def _read_results_pickle(results_file: Path) -> List:
|
||||
"""
|
||||
Read hyperopt results from pickle file
|
||||
LEGACY method - new files are written as json and cannot be read with this method.
|
||||
"""
|
||||
from joblib import load
|
||||
|
||||
logger.info(f"Reading pickled epochs from '{results_file}'")
|
||||
data = load(results_file)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _read_results(results_file: Path) -> List:
|
||||
"""
|
||||
Read hyperopt results from file
|
||||
"""
|
||||
logger.info("Reading epochs from '%s'", results_file)
|
||||
data = load(results_file)
|
||||
import rapidjson
|
||||
logger.info(f"Reading epochs from '{results_file}'")
|
||||
with results_file.open('r') as f:
|
||||
data = [rapidjson.loads(line) for line in f]
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
@@ -37,7 +60,10 @@ class HyperoptTools():
|
||||
"""
|
||||
epochs: List = []
|
||||
if results_file.is_file() and results_file.stat().st_size > 0:
|
||||
epochs = HyperoptTools._read_results(results_file)
|
||||
if results_file.suffix == '.pickle':
|
||||
epochs = HyperoptTools._read_results_pickle(results_file)
|
||||
else:
|
||||
epochs = HyperoptTools._read_results(results_file)
|
||||
# Detection of some old format, without 'is_best' field saved
|
||||
if epochs[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
@@ -53,6 +79,7 @@ class HyperoptTools():
|
||||
Display details of the hyperopt result
|
||||
"""
|
||||
params = results.get('params_details', {})
|
||||
non_optimized = results.get('params_not_optimized', {})
|
||||
|
||||
# Default header string
|
||||
if header_str is None:
|
||||
@@ -69,8 +96,10 @@ class HyperoptTools():
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||
|
||||
else:
|
||||
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:")
|
||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:")
|
||||
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:",
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:")
|
||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:")
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||
@@ -96,12 +125,12 @@ class HyperoptTools():
|
||||
result_dict.update(space_params)
|
||||
|
||||
@staticmethod
|
||||
def _params_pretty_print(params, space: str, header: str) -> None:
|
||||
if space in params:
|
||||
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
|
||||
if space in params or space in non_optimized:
|
||||
space_params = HyperoptTools._space_params(params, space, 5)
|
||||
params_result = f"\n# {header}\n"
|
||||
result = f"\n# {header}\n"
|
||||
if space == 'stoploss':
|
||||
params_result += f"stoploss = {space_params.get('stoploss')}"
|
||||
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)
|
||||
@@ -110,28 +139,64 @@ class HyperoptTools():
|
||||
(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}"
|
||||
result += f"minimal_roi = {minimal_roi_result}"
|
||||
elif space == 'trailing':
|
||||
|
||||
for k, v in space_params.items():
|
||||
params_result += f'{k} = {v}\n'
|
||||
result += f'{k} = {v}\n'
|
||||
|
||||
else:
|
||||
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
|
||||
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
|
||||
no_params = HyperoptTools._space_params(non_optimized, space, 5)
|
||||
|
||||
params_result = params_result.replace("\n", "\n ")
|
||||
print(params_result)
|
||||
result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}"
|
||||
|
||||
result = result.replace("\n", "\n ")
|
||||
print(result)
|
||||
|
||||
@staticmethod
|
||||
def _space_params(params, space: str, r: int = None) -> Dict:
|
||||
d = params[space]
|
||||
# Round floats to `r` digits after the decimal point if requested
|
||||
return round_dict(d, r) if r else d
|
||||
d = params.get(space)
|
||||
if d:
|
||||
# Round floats to `r` digits after the decimal point if requested
|
||||
return round_dict(d, r) if r else d
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _pprint(params, non_optimized, indent: int = 4):
|
||||
"""
|
||||
Pretty-print hyperopt results (based on 2 dicts - with add. comment)
|
||||
"""
|
||||
p = params.copy()
|
||||
p.update(non_optimized)
|
||||
result = '{\n'
|
||||
|
||||
for k, param in p.items():
|
||||
result += " " * indent + f'"{k}": '
|
||||
result += f'"{param}",' if isinstance(param, str) else f'{param},'
|
||||
if k in non_optimized:
|
||||
result += " # value loaded from strategy"
|
||||
result += "\n"
|
||||
result += '}'
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def is_best_loss(results, current_best_loss: float) -> bool:
|
||||
return results['loss'] < current_best_loss
|
||||
return bool(results['loss'] < current_best_loss)
|
||||
|
||||
@staticmethod
|
||||
def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str:
|
||||
"""
|
||||
Return the formatted results explanation in a string
|
||||
"""
|
||||
return (f"{results_metrics['total_trades']:6d} trades. "
|
||||
f"{results_metrics['wins']}/{results_metrics['draws']}"
|
||||
f"/{results_metrics['losses']} Wins/Draws/Losses. "
|
||||
f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. "
|
||||
f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. "
|
||||
f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} "
|
||||
f"({results_metrics['profit_total'] * 100: 7.2f}%). "
|
||||
f"Avg duration {results_metrics['holding_avg']} min."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_explanation_string(results, total_epochs) -> str:
|
||||
@@ -156,12 +221,27 @@ class HyperoptTools():
|
||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||
# Ensure compatibility with older versions of hyperopt results
|
||||
trials['results_metrics.winsdrawslosses'] = 'N/A'
|
||||
legacy_mode = True
|
||||
|
||||
if 'results_metrics.total_trades' in trials:
|
||||
legacy_mode = False
|
||||
# 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',
|
||||
'results_metrics.winsdrawslosses',
|
||||
'results_metrics.avg_profit', 'results_metrics.total_profit',
|
||||
'results_metrics.profit', 'results_metrics.duration',
|
||||
'loss', 'is_initial_point', 'is_best']]
|
||||
|
||||
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.winsdrawslosses',
|
||||
'results_metrics.avg_profit', 'results_metrics.total_profit',
|
||||
'results_metrics.profit', 'results_metrics.duration',
|
||||
'loss', 'is_initial_point', 'is_best']]
|
||||
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
|
||||
'Total profit', 'Profit', 'Avg duration', 'Objective',
|
||||
'is_initial_point', 'is_best']
|
||||
@@ -171,26 +251,28 @@ class HyperoptTools():
|
||||
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)
|
||||
|
||||
perc_multi = 1 if legacy_mode else 100
|
||||
trials['Epoch'] = trials['Epoch'].apply(
|
||||
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
|
||||
)
|
||||
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 * perc_multi:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
|
||||
)
|
||||
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(
|
||||
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(
|
||||
lambda x: '{:,.8f} {} {}'.format(
|
||||
x['Total profit'], config['stake_currency'],
|
||||
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
|
||||
).rjust(25+len(config['stake_currency']))
|
||||
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])),
|
||||
lambda x: '{} {}'.format(
|
||||
round_coin_value(x['Total profit'], stake_currency),
|
||||
'({:,.2f}%)'.format(x['Profit'] * perc_multi).rjust(10, ' ')
|
||||
).rjust(25+len(stake_currency))
|
||||
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),
|
||||
axis=1
|
||||
)
|
||||
trials = trials.drop(columns=['Total profit'])
|
||||
@@ -251,11 +333,21 @@ class HyperoptTools():
|
||||
trials['Best'] = ''
|
||||
trials['Stake currency'] = config['stake_currency']
|
||||
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.avg_profit', 'results_metrics.median_profit',
|
||||
'results_metrics.total_profit',
|
||||
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
if 'results_metrics.total_trades' in trials:
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||
'results_metrics.profit_total',
|
||||
'Stake currency',
|
||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
perc_multi = 100
|
||||
else:
|
||||
perc_multi = 1
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.avg_profit', 'results_metrics.median_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]
|
||||
|
||||
@@ -272,21 +364,24 @@ class HyperoptTools():
|
||||
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
|
||||
trials['Epoch'] = trials['Epoch'].astype(str)
|
||||
trials['Trades'] = trials['Trades'].astype(str)
|
||||
trials['Median profit'] = trials['Median profit'] * perc_multi
|
||||
|
||||
trials['Total profit'] = trials['Total profit'].apply(
|
||||
lambda x: '{:,.8f}'.format(x) if x != 0.0 else ""
|
||||
lambda x: f'{x:,.8f}' if x != 0.0 else ""
|
||||
)
|
||||
trials['Profit'] = trials['Profit'].apply(
|
||||
lambda x: '{:,.2f}'.format(x) if not isna(x) else ""
|
||||
lambda x: f'{x:,.2f}' if not isna(x) else ""
|
||||
)
|
||||
trials['Avg profit'] = trials['Avg profit'].apply(
|
||||
lambda x: '{:,.2f}%'.format(x) if not isna(x) else ""
|
||||
)
|
||||
trials['Avg duration'] = trials['Avg duration'].apply(
|
||||
lambda x: '{:,.1f} m'.format(x) if not isna(x) else ""
|
||||
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
|
||||
)
|
||||
if perc_multi == 1:
|
||||
trials['Avg duration'] = trials['Avg duration'].apply(
|
||||
lambda x: f'{x:,.1f} m' if isinstance(
|
||||
x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else ""
|
||||
)
|
||||
trials['Objective'] = trials['Objective'].apply(
|
||||
lambda x: '{:,.5f}'.format(x) if x != 100000 else ""
|
||||
lambda x: f'{x:,.5f}' if x != 100000 else ""
|
||||
)
|
||||
|
||||
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
|
||||
|
||||
Reference in New Issue
Block a user