2021-03-17 19:43:51 +00:00
|
|
|
import logging
|
2021-05-29 14:49:28 +00:00
|
|
|
from copy import deepcopy
|
2021-06-29 18:39:07 +00:00
|
|
|
from datetime import datetime, timezone
|
2021-03-17 19:43:51 +00:00
|
|
|
from pathlib import Path
|
2021-08-10 07:48:26 +00:00
|
|
|
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
2021-03-17 19:43:51 +00:00
|
|
|
|
2021-06-29 18:39:07 +00:00
|
|
|
import numpy as np
|
2021-09-17 15:42:33 +00:00
|
|
|
import pandas as pd
|
2021-03-17 19:43:51 +00:00
|
|
|
import rapidjson
|
|
|
|
import tabulate
|
|
|
|
from colorama import Fore, Style
|
|
|
|
from pandas import isna, json_normalize
|
|
|
|
|
2022-10-14 14:41:25 +00:00
|
|
|
from freqtrade.constants import FTHYPT_FILEVERSION, Config
|
2022-08-19 13:11:43 +00:00
|
|
|
from freqtrade.enums import HyperoptState
|
2021-03-17 19:43:51 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
2024-01-06 11:46:30 +00:00
|
|
|
from freqtrade.misc import deep_merge_dicts, round_dict, safe_value_fallback2
|
2021-08-08 08:05:28 +00:00
|
|
|
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
|
2022-11-25 15:31:21 +00:00
|
|
|
from freqtrade.optimize.optimize_reports import generate_wins_draws_losses
|
2024-01-06 15:02:47 +00:00
|
|
|
from freqtrade.util import fmt_coin
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2021-06-15 18:15:37 +00:00
|
|
|
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
|
|
|
|
|
2023-04-04 18:04:28 +00:00
|
|
|
HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
|
|
|
|
2021-03-17 19:43:51 +00:00
|
|
|
|
2021-06-30 17:48:34 +00:00
|
|
|
def hyperopt_serializer(x):
|
2021-06-29 18:39:07 +00:00
|
|
|
if isinstance(x, np.integer):
|
|
|
|
return int(x)
|
2021-07-02 18:52:25 +00:00
|
|
|
if isinstance(x, np.bool_):
|
|
|
|
return bool(x)
|
|
|
|
|
2021-06-29 18:39:07 +00:00
|
|
|
return str(x)
|
|
|
|
|
|
|
|
|
2023-06-29 12:16:10 +00:00
|
|
|
class HyperoptStateContainer:
|
2024-05-12 15:16:02 +00:00
|
|
|
"""Singleton class to track state of hyperopt"""
|
|
|
|
|
2022-08-20 08:55:52 +00:00
|
|
|
state: HyperoptState = HyperoptState.OPTIMIZE
|
2022-08-19 13:11:43 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def set_state(cls, value: HyperoptState):
|
2022-08-20 08:55:52 +00:00
|
|
|
cls.state = value
|
2022-08-19 13:11:43 +00:00
|
|
|
|
|
|
|
|
2023-06-29 12:16:10 +00:00
|
|
|
class HyperoptTools:
|
2021-05-29 14:49:28 +00:00
|
|
|
@staticmethod
|
2022-09-18 11:20:36 +00:00
|
|
|
def get_strategy_filename(config: Config, strategy_name: str) -> Optional[Path]:
|
2021-05-29 14:49:28 +00:00
|
|
|
"""
|
|
|
|
Get Strategy-location (filename) from strategy_name
|
|
|
|
"""
|
|
|
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
2024-05-12 15:16:02 +00:00
|
|
|
|
2022-04-23 07:11:50 +00:00
|
|
|
strategy_objs = StrategyResolver.search_all_objects(
|
2024-05-12 15:16:02 +00:00
|
|
|
config, False, config.get("recursive_strategy_search", False)
|
|
|
|
)
|
|
|
|
strategies = [s for s in strategy_objs if s["name"] == strategy_name]
|
2021-05-29 14:56:36 +00:00
|
|
|
if strategies:
|
|
|
|
strategy = strategies[0]
|
2021-05-29 14:49:28 +00:00
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
return Path(strategy["location"])
|
2021-05-29 14:56:36 +00:00
|
|
|
return None
|
2021-05-29 14:49:28 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def export_params(params, strategy_name: str, filename: Path):
|
|
|
|
"""
|
|
|
|
Generate files
|
|
|
|
"""
|
2024-05-12 15:16:02 +00:00
|
|
|
final_params = deepcopy(params["params_not_optimized"])
|
|
|
|
final_params = deep_merge_dicts(params["params_details"], final_params)
|
2021-05-29 14:49:28 +00:00
|
|
|
final_params = {
|
2024-05-12 15:16:02 +00:00
|
|
|
"strategy_name": strategy_name,
|
|
|
|
"params": final_params,
|
|
|
|
"ft_stratparam_v": 1,
|
|
|
|
"export_time": datetime.now(timezone.utc),
|
2021-05-29 14:49:28 +00:00
|
|
|
}
|
|
|
|
logger.info(f"Dumping parameters to {filename}")
|
2024-05-12 15:16:02 +00:00
|
|
|
with filename.open("w") as f:
|
|
|
|
rapidjson.dump(
|
|
|
|
final_params,
|
|
|
|
f,
|
|
|
|
indent=2,
|
|
|
|
default=hyperopt_serializer,
|
|
|
|
number_mode=HYPER_PARAMS_FILE_FORMAT,
|
|
|
|
)
|
2021-05-29 14:49:28 +00:00
|
|
|
|
2023-04-04 18:04:28 +00:00
|
|
|
@staticmethod
|
|
|
|
def load_params(filename: Path) -> Dict:
|
|
|
|
"""
|
|
|
|
Load parameters from file
|
|
|
|
"""
|
2024-05-12 15:16:02 +00:00
|
|
|
with filename.open("r") as f:
|
2023-04-04 18:04:28 +00:00
|
|
|
params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT)
|
|
|
|
return params
|
|
|
|
|
2021-06-29 18:22:30 +00:00
|
|
|
@staticmethod
|
2022-09-18 11:31:52 +00:00
|
|
|
def try_export_params(config: Config, strategy_name: str, params: Dict):
|
2024-05-12 15:16:02 +00:00
|
|
|
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get("disableparamexport", False):
|
2021-06-29 18:22:30 +00:00
|
|
|
# Export parameters ...
|
|
|
|
fn = HyperoptTools.get_strategy_filename(config, strategy_name)
|
|
|
|
if fn:
|
2024-05-12 15:16:02 +00:00
|
|
|
HyperoptTools.export_params(params, strategy_name, fn.with_suffix(".json"))
|
2021-06-29 18:22:30 +00:00
|
|
|
else:
|
2021-07-09 18:46:38 +00:00
|
|
|
logger.warning("Strategy not found, not exporting parameter file.")
|
2021-06-29 18:22:30 +00:00
|
|
|
|
2021-05-01 14:36:35 +00:00
|
|
|
@staticmethod
|
2022-09-18 11:31:52 +00:00
|
|
|
def has_space(config: Config, space: str) -> bool:
|
2021-05-01 14:36:35 +00:00
|
|
|
"""
|
|
|
|
Tell if the space value is contained in the configuration
|
|
|
|
"""
|
2021-08-03 05:14:31 +00:00
|
|
|
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
2024-05-12 15:16:02 +00:00
|
|
|
if space in ("trailing", "protection", "trades"):
|
|
|
|
return any(s in config["spaces"] for s in [space, "all"])
|
2021-05-01 14:36:35 +00:00
|
|
|
else:
|
2024-05-12 15:16:02 +00:00
|
|
|
return any(s in config["spaces"] for s in [space, "all", "default"])
|
2021-05-01 14:36:35 +00:00
|
|
|
|
2021-05-12 04:06:30 +00:00
|
|
|
@staticmethod
|
2021-08-10 07:48:26 +00:00
|
|
|
def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]:
|
2021-05-12 04:06:30 +00:00
|
|
|
"""
|
2021-08-10 07:48:26 +00:00
|
|
|
Stream hyperopt results from file
|
2021-05-12 04:06:30 +00:00
|
|
|
"""
|
|
|
|
import rapidjson
|
2024-05-12 15:16:02 +00:00
|
|
|
|
2021-05-12 04:06:30 +00:00
|
|
|
logger.info(f"Reading epochs from '{results_file}'")
|
2024-05-12 15:16:02 +00:00
|
|
|
with results_file.open("r") as f:
|
2021-08-10 07:48:26 +00:00
|
|
|
data = []
|
|
|
|
for line in f:
|
|
|
|
data += [rapidjson.loads(line)]
|
|
|
|
if len(data) >= batch_size:
|
|
|
|
yield data
|
|
|
|
data = []
|
|
|
|
yield data
|
2021-05-12 04:06:30 +00:00
|
|
|
|
2021-03-17 19:43:51 +00:00
|
|
|
@staticmethod
|
2021-08-10 07:48:26 +00:00
|
|
|
def _test_hyperopt_results_exist(results_file) -> bool:
|
2021-03-17 19:43:51 +00:00
|
|
|
if results_file.is_file() and results_file.stat().st_size > 0:
|
2024-05-12 15:16:02 +00:00
|
|
|
if results_file.suffix == ".pickle":
|
2021-08-09 05:03:13 +00:00
|
|
|
raise OperationalException(
|
|
|
|
"Legacy hyperopt results are no longer supported."
|
|
|
|
"Please rerun hyperopt or use an older version to load this file."
|
|
|
|
)
|
2021-08-10 07:48:26 +00:00
|
|
|
return True
|
|
|
|
else:
|
|
|
|
# No file found.
|
|
|
|
return False
|
2021-03-17 19:43:51 +00:00
|
|
|
|
2021-08-08 08:05:28 +00:00
|
|
|
@staticmethod
|
2022-09-18 11:31:52 +00:00
|
|
|
def load_filtered_results(results_file: Path, config: Config) -> Tuple[List, int]:
|
2021-08-08 08:05:28 +00:00
|
|
|
filteroptions = {
|
2024-05-12 15:16:02 +00:00
|
|
|
"only_best": config.get("hyperopt_list_best", False),
|
|
|
|
"only_profitable": config.get("hyperopt_list_profitable", False),
|
|
|
|
"filter_min_trades": config.get("hyperopt_list_min_trades", 0),
|
|
|
|
"filter_max_trades": config.get("hyperopt_list_max_trades", 0),
|
|
|
|
"filter_min_avg_time": config.get("hyperopt_list_min_avg_time"),
|
|
|
|
"filter_max_avg_time": config.get("hyperopt_list_max_avg_time"),
|
|
|
|
"filter_min_avg_profit": config.get("hyperopt_list_min_avg_profit"),
|
|
|
|
"filter_max_avg_profit": config.get("hyperopt_list_max_avg_profit"),
|
|
|
|
"filter_min_total_profit": config.get("hyperopt_list_min_total_profit"),
|
|
|
|
"filter_max_total_profit": config.get("hyperopt_list_max_total_profit"),
|
|
|
|
"filter_min_objective": config.get("hyperopt_list_min_objective"),
|
|
|
|
"filter_max_objective": config.get("hyperopt_list_max_objective"),
|
2021-08-08 08:05:28 +00:00
|
|
|
}
|
2021-08-10 07:48:26 +00:00
|
|
|
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
|
|
|
# No file found.
|
2022-01-21 14:23:06 +00:00
|
|
|
logger.warning(f"Hyperopt file {results_file} not found.")
|
2021-08-10 07:48:26 +00:00
|
|
|
return [], 0
|
|
|
|
|
|
|
|
epochs = []
|
|
|
|
total_epochs = 0
|
|
|
|
for epochs_tmp in HyperoptTools._read_results(results_file):
|
2024-05-12 15:16:02 +00:00
|
|
|
if total_epochs == 0 and epochs_tmp[0].get("is_best") is None:
|
2021-08-10 07:48:26 +00:00
|
|
|
raise OperationalException(
|
|
|
|
"The file with HyperoptTools results is incompatible with this version "
|
2024-05-12 15:16:02 +00:00
|
|
|
"of Freqtrade and cannot be loaded."
|
|
|
|
)
|
2021-08-10 07:48:26 +00:00
|
|
|
total_epochs += len(epochs_tmp)
|
|
|
|
epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False)
|
2021-08-08 08:05:28 +00:00
|
|
|
|
2021-08-10 07:48:26 +00:00
|
|
|
logger.info(f"Loaded {total_epochs} previous evaluations from disk.")
|
2021-08-08 08:05:28 +00:00
|
|
|
|
2021-08-10 07:48:26 +00:00
|
|
|
# Final filter run ...
|
|
|
|
epochs = hyperopt_filter_epochs(epochs, filteroptions, log=True)
|
2021-08-08 08:05:28 +00:00
|
|
|
|
|
|
|
return epochs, total_epochs
|
|
|
|
|
2021-03-17 19:43:51 +00:00
|
|
|
@staticmethod
|
2024-05-12 15:16:02 +00:00
|
|
|
def show_epoch_details(
|
|
|
|
results,
|
|
|
|
total_epochs: int,
|
|
|
|
print_json: bool,
|
|
|
|
no_header: bool = False,
|
|
|
|
header_str: Optional[str] = None,
|
|
|
|
) -> None:
|
2021-03-17 19:43:51 +00:00
|
|
|
"""
|
|
|
|
Display details of the hyperopt result
|
|
|
|
"""
|
2024-05-12 15:16:02 +00:00
|
|
|
params = results.get("params_details", {})
|
|
|
|
non_optimized = results.get("params_not_optimized", {})
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
# Default header string
|
|
|
|
if header_str is None:
|
|
|
|
header_str = "Best result"
|
|
|
|
|
|
|
|
if not no_header:
|
|
|
|
explanation_str = HyperoptTools._format_explanation_string(results, total_epochs)
|
|
|
|
print(f"\n{header_str}:\n\n{explanation_str}\n")
|
|
|
|
|
|
|
|
if print_json:
|
|
|
|
result_dict: Dict = {}
|
2024-05-12 15:16:02 +00:00
|
|
|
for s in [
|
|
|
|
"buy",
|
|
|
|
"sell",
|
|
|
|
"protection",
|
|
|
|
"roi",
|
|
|
|
"stoploss",
|
|
|
|
"trailing",
|
|
|
|
"max_open_trades",
|
|
|
|
]:
|
2021-05-24 16:51:33 +00:00
|
|
|
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
2023-04-04 18:04:28 +00:00
|
|
|
print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT))
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
else:
|
2023-01-04 09:34:44 +00:00
|
|
|
HyperoptTools._params_pretty_print(
|
2024-05-12 15:16:02 +00:00
|
|
|
params, "buy", "Buy hyperspace params:", non_optimized
|
|
|
|
)
|
|
|
|
HyperoptTools._params_pretty_print(
|
|
|
|
params, "sell", "Sell hyperspace params:", non_optimized
|
|
|
|
)
|
|
|
|
HyperoptTools._params_pretty_print(
|
|
|
|
params, "protection", "Protection hyperspace params:", non_optimized
|
|
|
|
)
|
|
|
|
HyperoptTools._params_pretty_print(params, "roi", "ROI table:", non_optimized)
|
|
|
|
HyperoptTools._params_pretty_print(params, "stoploss", "Stoploss:", non_optimized)
|
|
|
|
HyperoptTools._params_pretty_print(params, "trailing", "Trailing stop:", non_optimized)
|
|
|
|
HyperoptTools._params_pretty_print(
|
|
|
|
params, "max_open_trades", "Max Open Trades:", non_optimized
|
|
|
|
)
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2021-05-24 16:51:33 +00:00
|
|
|
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
2021-06-18 20:03:04 +00:00
|
|
|
if (space in params) or (space in non_optimized):
|
2021-03-17 19:43:51 +00:00
|
|
|
space_params = HyperoptTools._space_params(params, space)
|
2021-05-24 16:51:33 +00:00
|
|
|
space_non_optimized = HyperoptTools._space_params(non_optimized, space)
|
|
|
|
all_space_params = space_params
|
|
|
|
|
2021-06-17 20:41:49 +00:00
|
|
|
# Merge non optimized params if there are any
|
2021-05-24 16:51:33 +00:00
|
|
|
if len(space_non_optimized) > 0:
|
2021-06-18 20:03:04 +00:00
|
|
|
all_space_params = {**space_params, **space_non_optimized}
|
2021-05-24 16:51:33 +00:00
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
if space in ["buy", "sell"]:
|
|
|
|
result_dict.setdefault("params", {}).update(all_space_params)
|
|
|
|
elif space == "roi":
|
2021-03-17 19:43:51 +00:00
|
|
|
# Convert keys in min_roi dict to strings because
|
|
|
|
# rapidjson cannot dump dicts with integer keys...
|
2024-05-12 15:16:02 +00:00
|
|
|
result_dict["minimal_roi"] = {str(k): v for k, v in all_space_params.items()}
|
2021-03-17 19:43:51 +00:00
|
|
|
else: # 'stoploss', 'trailing'
|
2021-05-24 16:51:33 +00:00
|
|
|
result_dict.update(all_space_params)
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2024-04-20 07:12:47 +00:00
|
|
|
def _params_pretty_print(
|
2024-05-12 15:16:02 +00:00
|
|
|
params, space: str, header: str, non_optimized: Optional[Dict] = None
|
|
|
|
) -> None:
|
2024-04-20 07:12:47 +00:00
|
|
|
if space in params or (non_optimized and space in non_optimized):
|
2021-03-17 19:43:51 +00:00
|
|
|
space_params = HyperoptTools._space_params(params, space, 5)
|
2021-06-15 18:15:37 +00:00
|
|
|
no_params = HyperoptTools._space_params(non_optimized, space, 5)
|
2024-05-12 15:16:02 +00:00
|
|
|
appendix = ""
|
2021-06-15 18:15:37 +00:00
|
|
|
if not space_params and not no_params:
|
|
|
|
# No parameters - don't print
|
|
|
|
return
|
|
|
|
if not space_params:
|
|
|
|
# Not optimized parameters - append string
|
2021-06-15 18:33:35 +00:00
|
|
|
appendix = NON_OPT_PARAM_APPENDIX
|
2021-06-15 18:15:37 +00:00
|
|
|
|
2021-05-02 09:30:53 +00:00
|
|
|
result = f"\n# {header}\n"
|
2021-06-14 18:45:06 +00:00
|
|
|
if space == "stoploss":
|
2021-06-15 18:15:37 +00:00
|
|
|
stoploss = safe_value_fallback2(space_params, no_params, space, space)
|
2024-05-12 15:16:02 +00:00
|
|
|
result += f"stoploss = {stoploss}{appendix}"
|
2023-01-04 09:34:44 +00:00
|
|
|
elif space == "max_open_trades":
|
|
|
|
max_open_trades = safe_value_fallback2(space_params, no_params, space, space)
|
2024-05-12 15:16:02 +00:00
|
|
|
result += f"max_open_trades = {max_open_trades}{appendix}"
|
2021-06-14 18:45:06 +00:00
|
|
|
elif space == "roi":
|
2024-05-12 15:16:02 +00:00
|
|
|
result = result[:-1] + f"{appendix}\n"
|
|
|
|
minimal_roi_result = rapidjson.dumps(
|
|
|
|
{str(k): v for k, v in (space_params or no_params).items()},
|
|
|
|
default=str,
|
|
|
|
indent=4,
|
|
|
|
number_mode=rapidjson.NM_NATIVE,
|
|
|
|
)
|
2021-05-02 09:30:53 +00:00
|
|
|
result += f"minimal_roi = {minimal_roi_result}"
|
2021-06-14 18:45:06 +00:00
|
|
|
elif space == "trailing":
|
2021-06-15 18:15:37 +00:00
|
|
|
for k, v in (space_params or no_params).items():
|
2021-06-15 18:33:35 +00:00
|
|
|
result += f"{k} = {v}{appendix}\n"
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
else:
|
2021-06-14 18:45:06 +00:00
|
|
|
# Buy / sell parameters
|
2021-05-02 09:30:53 +00:00
|
|
|
|
2021-06-15 18:33:35 +00:00
|
|
|
result += f"{space}_params = {HyperoptTools._pprint_dict(space_params, no_params)}"
|
2021-03-17 19:43:51 +00:00
|
|
|
|
2021-05-02 09:30:53 +00:00
|
|
|
result = result.replace("\n", "\n ")
|
|
|
|
print(result)
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2023-01-21 14:01:56 +00:00
|
|
|
def _space_params(params, space: str, r: Optional[int] = None) -> Dict:
|
2021-05-02 09:30:53 +00:00
|
|
|
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
|
2021-06-15 18:33:35 +00:00
|
|
|
def _pprint_dict(params, non_optimized, indent: int = 4):
|
2021-05-02 09:30:53 +00:00
|
|
|
"""
|
|
|
|
Pretty-print hyperopt results (based on 2 dicts - with add. comment)
|
|
|
|
"""
|
|
|
|
p = params.copy()
|
|
|
|
p.update(non_optimized)
|
2024-05-12 15:16:02 +00:00
|
|
|
result = "{\n"
|
2021-05-02 09:30:53 +00:00
|
|
|
|
|
|
|
for k, param in p.items():
|
2021-05-07 18:23:11 +00:00
|
|
|
result += " " * indent + f'"{k}": '
|
2024-05-12 15:16:02 +00:00
|
|
|
result += f'"{param}",' if isinstance(param, str) else f"{param},"
|
2021-05-02 09:30:53 +00:00
|
|
|
if k in non_optimized:
|
2021-06-15 18:15:37 +00:00
|
|
|
result += NON_OPT_PARAM_APPENDIX
|
2021-05-02 09:30:53 +00:00
|
|
|
result += "\n"
|
2024-05-12 15:16:02 +00:00
|
|
|
result += "}"
|
2021-05-02 09:30:53 +00:00
|
|
|
return result
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def is_best_loss(results, current_best_loss: float) -> bool:
|
2024-05-12 15:16:02 +00:00
|
|
|
return bool(results["loss"] < current_best_loss)
|
2021-03-17 19:43:51 +00:00
|
|
|
|
2021-05-01 07:05:46 +00:00
|
|
|
@staticmethod
|
|
|
|
def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str:
|
|
|
|
"""
|
|
|
|
Return the formatted results explanation in a string
|
|
|
|
"""
|
2024-05-12 15:16:02 +00:00
|
|
|
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']:7.2%}. "
|
|
|
|
f"Median profit {results_metrics['profit_median']:7.2%}. "
|
|
|
|
f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} "
|
|
|
|
f"({results_metrics['profit_total']:8.2%}). "
|
|
|
|
f"Avg duration {results_metrics['holding_avg']} min."
|
|
|
|
)
|
2021-05-01 07:05:46 +00:00
|
|
|
|
2021-03-17 19:43:51 +00:00
|
|
|
@staticmethod
|
|
|
|
def _format_explanation_string(results, total_epochs) -> str:
|
2024-05-12 15:16:02 +00:00
|
|
|
return (
|
|
|
|
("*" if results["is_initial_point"] else " ")
|
|
|
|
+ f"{results['current_epoch']:5d}/{total_epochs}: "
|
|
|
|
+ f"{results['results_explanation']} "
|
|
|
|
+ f"Objective: {results['loss']:.5f}"
|
|
|
|
)
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2024-05-19 15:57:05 +00:00
|
|
|
def prepare_trials_columns(trials: pd.DataFrame) -> pd.DataFrame:
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Best"] = ""
|
2021-06-09 15:03:24 +00:00
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
if "results_metrics.winsdrawslosses" not in trials.columns:
|
2021-03-17 19:43:51 +00:00
|
|
|
# Ensure compatibility with older versions of hyperopt results
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["results_metrics.winsdrawslosses"] = "N/A"
|
2021-03-17 19:43:51 +00:00
|
|
|
|
2024-05-19 15:57:05 +00:00
|
|
|
has_account_drawdown = "results_metrics.max_drawdown_account" in trials.columns
|
|
|
|
if not has_account_drawdown:
|
2021-06-08 05:42:55 +00:00
|
|
|
# Ensure compatibility with older versions of hyperopt results
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["results_metrics.max_drawdown_account"] = None
|
|
|
|
if "is_random" not in trials.columns:
|
|
|
|
trials["is_random"] = False
|
2021-06-08 05:42:55 +00:00
|
|
|
|
2022-01-04 15:59:58 +00:00
|
|
|
# New mode, using backtest result for metrics
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["results_metrics.winsdrawslosses"] = trials.apply(
|
2022-11-25 15:31:21 +00:00
|
|
|
lambda x: generate_wins_draws_losses(
|
2024-05-12 15:16:02 +00:00
|
|
|
x["results_metrics.wins"], x["results_metrics.draws"], x["results_metrics.losses"]
|
|
|
|
),
|
|
|
|
axis=1,
|
|
|
|
)
|
2021-04-28 20:33:58 +00:00
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
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",
|
|
|
|
"results_metrics.max_drawdown_account",
|
|
|
|
"results_metrics.max_drawdown_abs",
|
|
|
|
"loss",
|
|
|
|
"is_initial_point",
|
|
|
|
"is_random",
|
|
|
|
"is_best",
|
2022-01-04 16:31:59 +00:00
|
|
|
]
|
2024-05-12 15:16:02 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
trials.columns = [
|
|
|
|
"Best",
|
|
|
|
"Epoch",
|
|
|
|
"Trades",
|
|
|
|
" Win Draw Loss Win%",
|
|
|
|
"Avg profit",
|
|
|
|
"Total profit",
|
|
|
|
"Profit",
|
|
|
|
"Avg duration",
|
|
|
|
"max_drawdown_account",
|
|
|
|
"max_drawdown_abs",
|
|
|
|
"Objective",
|
|
|
|
"is_initial_point",
|
|
|
|
"is_random",
|
|
|
|
"is_best",
|
|
|
|
]
|
2021-03-17 19:43:51 +00:00
|
|
|
|
2021-06-09 15:03:24 +00:00
|
|
|
return trials
|
|
|
|
|
|
|
|
@staticmethod
|
2024-05-12 15:16:02 +00:00
|
|
|
def get_result_table(
|
|
|
|
config: Config,
|
|
|
|
results: list,
|
|
|
|
total_epochs: int,
|
|
|
|
highlight_best: bool,
|
|
|
|
print_colorized: bool,
|
|
|
|
remove_header: int,
|
|
|
|
) -> str:
|
2021-06-09 15:03:24 +00:00
|
|
|
"""
|
|
|
|
Log result table
|
|
|
|
"""
|
|
|
|
if not results:
|
2024-05-12 15:16:02 +00:00
|
|
|
return ""
|
2021-06-09 15:03:24 +00:00
|
|
|
|
|
|
|
tabulate.PRESERVE_WHITESPACE = True
|
|
|
|
trials = json_normalize(results, max_level=1)
|
|
|
|
|
2024-05-19 15:57:05 +00:00
|
|
|
trials = HyperoptTools.prepare_trials_columns(trials)
|
2021-06-09 15:03:24 +00:00
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["is_profit"] = False
|
|
|
|
trials.loc[trials["is_initial_point"] | trials["is_random"], "Best"] = "* "
|
|
|
|
trials.loc[trials["is_best"], "Best"] = "Best"
|
2022-03-29 23:29:14 +00:00
|
|
|
trials.loc[
|
2024-05-12 15:16:02 +00:00
|
|
|
(trials["is_initial_point"] | trials["is_random"]) & trials["is_best"], "Best"
|
|
|
|
] = "* Best"
|
|
|
|
trials.loc[trials["Total profit"] > 0, "is_profit"] = True
|
|
|
|
trials["Trades"] = trials["Trades"].astype(str)
|
2022-01-04 15:59:58 +00:00
|
|
|
# perc_multi = 1 if legacy_mode else 100
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Epoch"] = trials["Epoch"].apply(
|
|
|
|
lambda x: "{}/{}".format(str(x).rjust(len(str(total_epochs)), " "), total_epochs)
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Avg profit"] = trials["Avg profit"].apply(
|
|
|
|
lambda x: f"{x:,.2%}".rjust(7, " ") if not isna(x) else "--".rjust(7, " ")
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Avg duration"] = trials["Avg duration"].apply(
|
2024-05-13 17:49:15 +00:00
|
|
|
lambda x: (
|
|
|
|
f"{x:,.1f} m".rjust(7, " ")
|
|
|
|
if isinstance(x, float)
|
|
|
|
else f"{x}"
|
|
|
|
if not isna(x)
|
|
|
|
else "--".rjust(7, " ")
|
|
|
|
)
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Objective"] = trials["Objective"].apply(
|
|
|
|
lambda x: f"{x:,.5f}".rjust(8, " ") if x != 100000 else "N/A".rjust(8, " ")
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
stake_currency = config["stake_currency"]
|
2021-06-07 21:15:26 +00:00
|
|
|
|
2024-05-19 15:57:05 +00:00
|
|
|
trials["Max Drawdown (Acct)"] = trials.apply(
|
2024-05-13 17:49:15 +00:00
|
|
|
lambda x: (
|
|
|
|
"{} {}".format(
|
|
|
|
fmt_coin(x["max_drawdown_abs"], stake_currency, keep_trailing_zeros=True),
|
2024-05-19 15:57:05 +00:00
|
|
|
(f"({x['max_drawdown_account']:,.2%})").rjust(10, " "),
|
2024-05-13 17:49:15 +00:00
|
|
|
).rjust(25 + len(stake_currency))
|
2024-05-19 15:57:05 +00:00
|
|
|
if x["max_drawdown_account"] != 0.0
|
2024-05-13 17:49:15 +00:00
|
|
|
else "--".rjust(25 + len(stake_currency))
|
|
|
|
),
|
2024-05-12 15:16:02 +00:00
|
|
|
axis=1,
|
2022-01-04 16:31:59 +00:00
|
|
|
)
|
2021-06-08 05:42:55 +00:00
|
|
|
|
2024-05-19 15:57:05 +00:00
|
|
|
trials = trials.drop(columns=["max_drawdown_abs", "max_drawdown_account"])
|
2021-06-07 20:53:19 +00:00
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Profit"] = trials.apply(
|
2024-05-13 17:49:15 +00:00
|
|
|
lambda x: (
|
|
|
|
"{} {}".format(
|
|
|
|
fmt_coin(x["Total profit"], stake_currency, keep_trailing_zeros=True),
|
|
|
|
f"({x['Profit']:,.2%})".rjust(10, " "),
|
|
|
|
).rjust(25 + len(stake_currency))
|
|
|
|
if x["Total profit"] != 0.0
|
|
|
|
else "--".rjust(25 + len(stake_currency))
|
|
|
|
),
|
2024-05-12 15:16:02 +00:00
|
|
|
axis=1,
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
2024-05-12 15:16:02 +00:00
|
|
|
trials = trials.drop(columns=["Total profit"])
|
2021-03-17 19:43:51 +00:00
|
|
|
|
|
|
|
if print_colorized:
|
2023-10-30 18:16:22 +00:00
|
|
|
trials2 = trials.astype(str)
|
2021-03-17 19:43:51 +00:00
|
|
|
for i in range(len(trials)):
|
2024-05-12 15:16:02 +00:00
|
|
|
if trials.loc[i]["is_profit"]:
|
2022-04-11 16:02:02 +00:00
|
|
|
for j in range(len(trials.loc[i]) - 3):
|
2023-10-30 18:16:22 +00:00
|
|
|
trials2.iat[i, j] = f"{Fore.GREEN}{str(trials.iloc[i, j])}{Fore.RESET}"
|
2024-05-12 15:16:02 +00:00
|
|
|
if trials.loc[i]["is_best"] and highlight_best:
|
2022-04-11 16:02:02 +00:00
|
|
|
for j in range(len(trials.loc[i]) - 3):
|
2023-10-30 18:16:22 +00:00
|
|
|
trials2.iat[i, j] = (
|
|
|
|
f"{Style.BRIGHT}{str(trials.iloc[i, j])}{Style.RESET_ALL}"
|
|
|
|
)
|
|
|
|
trials = trials2
|
|
|
|
del trials2
|
2024-05-12 15:16:02 +00:00
|
|
|
trials = trials.drop(columns=["is_initial_point", "is_best", "is_profit", "is_random"])
|
2021-03-17 19:43:51 +00:00
|
|
|
if remove_header > 0:
|
|
|
|
table = tabulate.tabulate(
|
2024-05-12 15:16:02 +00:00
|
|
|
trials.to_dict(orient="list"), tablefmt="orgtbl", headers="keys", stralign="right"
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
table = table.split("\n", remove_header)[remove_header]
|
|
|
|
elif remove_header < 0:
|
|
|
|
table = tabulate.tabulate(
|
2024-05-12 15:16:02 +00:00
|
|
|
trials.to_dict(orient="list"), tablefmt="psql", headers="keys", stralign="right"
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
|
|
|
table = "\n".join(table.split("\n")[0:remove_header])
|
|
|
|
else:
|
|
|
|
table = tabulate.tabulate(
|
2024-05-12 15:16:02 +00:00
|
|
|
trials.to_dict(orient="list"), tablefmt="psql", headers="keys", stralign="right"
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
|
|
|
return table
|
|
|
|
|
|
|
|
@staticmethod
|
2022-09-18 11:31:52 +00:00
|
|
|
def export_csv_file(config: Config, results: list, csv_file: str) -> None:
|
2021-03-17 19:43:51 +00:00
|
|
|
"""
|
|
|
|
Log result to csv-file
|
|
|
|
"""
|
|
|
|
if not results:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Verification for overwrite
|
|
|
|
if Path(csv_file).is_file():
|
|
|
|
logger.error(f"CSV file already exists: {csv_file}")
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
2024-05-12 15:16:02 +00:00
|
|
|
Path(csv_file).open("w+").close()
|
2023-03-19 16:57:56 +00:00
|
|
|
except OSError:
|
2021-03-17 19:43:51 +00:00
|
|
|
logger.error(f"Failed to create CSV file: {csv_file}")
|
|
|
|
return
|
|
|
|
|
|
|
|
trials = json_normalize(results, max_level=1)
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Best"] = ""
|
|
|
|
trials["Stake currency"] = config["stake_currency"]
|
|
|
|
|
|
|
|
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",
|
|
|
|
"results_metrics.trade_count_long",
|
|
|
|
"results_metrics.trade_count_short",
|
|
|
|
"loss",
|
|
|
|
"is_initial_point",
|
|
|
|
"is_best",
|
|
|
|
]
|
2021-08-08 09:02:54 +00:00
|
|
|
perc_multi = 100
|
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
param_metrics = [("params_dict." + param) for param in results[0]["params_dict"].keys()]
|
2021-03-17 19:43:51 +00:00
|
|
|
trials = trials[base_metrics + param_metrics]
|
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
base_columns = [
|
|
|
|
"Best",
|
|
|
|
"Epoch",
|
|
|
|
"Trades",
|
|
|
|
"Avg profit",
|
|
|
|
"Median profit",
|
|
|
|
"Total profit",
|
|
|
|
"Stake currency",
|
|
|
|
"Profit",
|
|
|
|
"Avg duration",
|
|
|
|
"Trade count long",
|
|
|
|
"Trade count short",
|
|
|
|
"Objective",
|
|
|
|
"is_initial_point",
|
|
|
|
"is_best",
|
|
|
|
]
|
|
|
|
param_columns = list(results[0]["params_dict"].keys())
|
2021-03-17 19:43:51 +00:00
|
|
|
trials.columns = base_columns + param_columns
|
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
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)
|
|
|
|
trials["Median profit"] = trials["Median profit"] * perc_multi
|
|
|
|
|
|
|
|
trials["Total profit"] = trials["Total profit"].apply(
|
|
|
|
lambda x: f"{x:,.8f}" if x != 0.0 else ""
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Profit"] = trials["Profit"].apply(lambda x: f"{x:,.2f}" if not isna(x) else "")
|
|
|
|
trials["Avg profit"] = trials["Avg profit"].apply(
|
|
|
|
lambda x: f"{x * perc_multi:,.2f}%" if not isna(x) else ""
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
2024-05-12 15:16:02 +00:00
|
|
|
trials["Objective"] = trials["Objective"].apply(
|
|
|
|
lambda x: f"{x:,.5f}" if x != 100000 else ""
|
2021-03-17 19:43:51 +00:00
|
|
|
)
|
|
|
|
|
2024-05-12 15:16:02 +00:00
|
|
|
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")
|
2021-03-17 19:43:51 +00:00
|
|
|
logger.info(f"CSV file created: {csv_file}")
|