mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-12-15 04:11:14 +00:00
Merge pull request #12479 from freqtrade/feat/hyperopt_custom_spaces
Add support for custom hyperopt spaces
This commit is contained in:
@@ -5,7 +5,10 @@ Definition of cli arguments used in arguments.py
|
||||
from argparse import ArgumentTypeError
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.constants import HYPEROPT_BUILTIN_SPACES, HYPEROPT_LOSS_BUILTIN
|
||||
from freqtrade.constants import (
|
||||
HYPEROPT_BUILTIN_SPACE_OPTIONS,
|
||||
HYPEROPT_LOSS_BUILTIN,
|
||||
)
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
|
||||
@@ -278,9 +281,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"spaces": Arg(
|
||||
"--spaces",
|
||||
help="Specify which parameters to hyperopt. Space-separated list. Available options: "
|
||||
f"{', '.join(HYPEROPT_BUILTIN_SPACES)}. Default: `default` - "
|
||||
"which includes all spaces except for 'trailing', 'protection', and 'trades'.",
|
||||
help=(
|
||||
"Specify which parameters to hyperopt. Space-separated list. "
|
||||
"Available builtin options (custom spaces will not be listed here): "
|
||||
f"{', '.join(HYPEROPT_BUILTIN_SPACE_OPTIONS)}. Default: `default` - "
|
||||
"which includes all spaces except for 'trailing', 'protection', and 'trades'."
|
||||
),
|
||||
nargs="+",
|
||||
),
|
||||
"analyze_per_epoch": Arg(
|
||||
|
||||
@@ -101,7 +101,7 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None:
|
||||
names = [s["name"] for s in objs]
|
||||
objs_to_print: list[dict[str, Text | str]] = [
|
||||
{
|
||||
"name": Text(s["name"] if s["name"] else "--"),
|
||||
"Strategy name": Text(s["name"] if s["name"] else "--"),
|
||||
"location": s["location_rel"],
|
||||
"status": (
|
||||
Text("LOAD FAILED", style="bold red")
|
||||
@@ -115,11 +115,19 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None:
|
||||
]
|
||||
for idx, s in enumerate(objs):
|
||||
if "hyperoptable" in s:
|
||||
custom_params = [
|
||||
f"{space}: {len(params)}"
|
||||
for space, params in s["hyperoptable"].items()
|
||||
if space not in ["buy", "sell", "protection"]
|
||||
]
|
||||
hyp = s["hyperoptable"]
|
||||
objs_to_print[idx].update(
|
||||
{
|
||||
"hyperoptable": "Yes" if s["hyperoptable"]["count"] > 0 else "No",
|
||||
"buy-Params": str(len(s["hyperoptable"].get("buy", []))),
|
||||
"sell-Params": str(len(s["hyperoptable"].get("sell", []))),
|
||||
"hyperoptable": "Yes" if len(hyp) > 0 else "No",
|
||||
"buy-Params": str(len(hyp.get("buy", []))),
|
||||
"sell-Params": str(len(hyp.get("sell", []))),
|
||||
"protection-Params": str(len(hyp.get("protection", []))),
|
||||
"custom-Params": ", ".join(custom_params) if custom_params else "",
|
||||
}
|
||||
)
|
||||
table = Table()
|
||||
@@ -140,6 +148,7 @@ def start_list_strategies(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.hyper import detect_all_parameters
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
@@ -153,9 +162,9 @@ def start_list_strategies(args: dict[str, Any]) -> None:
|
||||
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
||||
for obj in strategy_objs:
|
||||
if obj["class"]:
|
||||
obj["hyperoptable"] = obj["class"].detect_all_parameters()
|
||||
obj["hyperoptable"] = detect_all_parameters(obj["class"])
|
||||
else:
|
||||
obj["hyperoptable"] = {"count": 0}
|
||||
obj["hyperoptable"] = {}
|
||||
|
||||
if args["print_one_column"]:
|
||||
print("\n".join([s["name"] for s in strategy_objs]))
|
||||
|
||||
@@ -8,7 +8,6 @@ from freqtrade.constants import (
|
||||
BACKTEST_CACHE_AGE,
|
||||
DRY_RUN_WALLET,
|
||||
EXPORT_OPTIONS,
|
||||
HYPEROPT_BUILTIN_SPACES,
|
||||
HYPEROPT_LOSS_BUILTIN,
|
||||
MARGIN_MODES,
|
||||
ORDERTIF_POSSIBILITIES,
|
||||
@@ -260,7 +259,7 @@ CONF_SCHEMA = {
|
||||
"includes all spaces except for 'trailing', 'protection', and 'trades'."
|
||||
),
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": HYPEROPT_BUILTIN_SPACES},
|
||||
"items": {"type": "string"},
|
||||
"default": ["default"],
|
||||
},
|
||||
"analyze_per_epoch": {
|
||||
|
||||
@@ -42,16 +42,17 @@ HYPEROPT_LOSS_BUILTIN = [
|
||||
"MultiMetricHyperOptLoss",
|
||||
]
|
||||
HYPEROPT_BUILTIN_SPACES = [
|
||||
"all",
|
||||
"buy",
|
||||
"sell",
|
||||
"enter",
|
||||
"exit",
|
||||
"roi",
|
||||
"stoploss",
|
||||
"trailing",
|
||||
"protection",
|
||||
"trades",
|
||||
"default",
|
||||
]
|
||||
HYPEROPT_BUILTIN_SPACE_OPTIONS = ["default", "all"] + HYPEROPT_BUILTIN_SPACES
|
||||
|
||||
AVAILABLE_PAIRLISTS = [
|
||||
"StaticPairList",
|
||||
|
||||
@@ -7,6 +7,7 @@ This module implements a convenience auto-hyperopt class, which can be used toge
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from typing import Literal
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
@@ -37,10 +38,17 @@ def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
|
||||
class HyperOptAuto(IHyperOpt):
|
||||
"""
|
||||
This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes.
|
||||
Most of the time Strategy.HyperOpt class would only implement indicator_space and
|
||||
sell_indicator_space methods, but other hyperopt methods can be overridden as well.
|
||||
Most of the time Strategy.HyperOpt class would only implement indicator_space and
|
||||
sell_indicator_space methods, but other hyperopt methods can be overridden as well.
|
||||
"""
|
||||
|
||||
def get_available_spaces(self) -> list[str]:
|
||||
"""
|
||||
Get list of available spaces defined in strategy.
|
||||
:return: list of available spaces.
|
||||
"""
|
||||
return list(self.strategy._ft_hyper_params)
|
||||
|
||||
def _get_func(self, name) -> Callable:
|
||||
"""
|
||||
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
|
||||
@@ -59,7 +67,13 @@ class HyperOptAuto(IHyperOpt):
|
||||
if attr.optimize:
|
||||
yield attr.get_space(attr_name)
|
||||
|
||||
def _get_indicator_space(self, category) -> list:
|
||||
def get_indicator_space(
|
||||
self, category: Literal["buy", "sell", "enter", "exit", "protection"] | str
|
||||
) -> list:
|
||||
"""
|
||||
Get indicator space for a given space.
|
||||
:param category: parameter space to get.
|
||||
"""
|
||||
# TODO: is this necessary, or can we call "generate_space" directly?
|
||||
indicator_space = list(self._generate_indicator_space(category))
|
||||
if len(indicator_space) > 0:
|
||||
@@ -70,15 +84,6 @@ class HyperOptAuto(IHyperOpt):
|
||||
)
|
||||
return []
|
||||
|
||||
def buy_indicator_space(self) -> list["Dimension"]:
|
||||
return self._get_indicator_space("buy")
|
||||
|
||||
def sell_indicator_space(self) -> list["Dimension"]:
|
||||
return self._get_indicator_space("sell")
|
||||
|
||||
def protection_space(self) -> list["Dimension"]:
|
||||
return self._get_indicator_space("protection")
|
||||
|
||||
def generate_roi_table(self, params: dict) -> dict[int, float]:
|
||||
return self._get_func("generate_roi_table")(params)
|
||||
|
||||
|
||||
@@ -70,13 +70,7 @@ class HyperOptimizer:
|
||||
"""
|
||||
|
||||
def __init__(self, config: Config, data_pickle_file: Path) -> None:
|
||||
self.buy_space: list[DimensionProtocol] = []
|
||||
self.sell_space: list[DimensionProtocol] = []
|
||||
self.protection_space: list[DimensionProtocol] = []
|
||||
self.roi_space: list[DimensionProtocol] = []
|
||||
self.stoploss_space: list[DimensionProtocol] = []
|
||||
self.trailing_space: list[DimensionProtocol] = []
|
||||
self.max_open_trades_space: list[DimensionProtocol] = []
|
||||
self.spaces: dict[str, list[DimensionProtocol]] = {}
|
||||
self.dimensions: list[DimensionProtocol] = []
|
||||
self.o_dimensions: dict = {}
|
||||
|
||||
@@ -167,37 +161,39 @@ class HyperOptimizer:
|
||||
"""
|
||||
result: dict = {}
|
||||
|
||||
if HyperoptTools.has_space(self.config, "buy"):
|
||||
result["buy"] = round_dict({p.name: params.get(p.name) for p in self.buy_space}, 13)
|
||||
if HyperoptTools.has_space(self.config, "sell"):
|
||||
result["sell"] = round_dict({p.name: params.get(p.name) for p in self.sell_space}, 13)
|
||||
if HyperoptTools.has_space(self.config, "protection"):
|
||||
result["protection"] = round_dict(
|
||||
{p.name: params.get(p.name) for p in self.protection_space}, 13
|
||||
)
|
||||
if HyperoptTools.has_space(self.config, "roi"):
|
||||
result["roi"] = round_dict(
|
||||
{str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items()}, 13
|
||||
)
|
||||
if HyperoptTools.has_space(self.config, "stoploss"):
|
||||
result["stoploss"] = round_dict(
|
||||
{p.name: params.get(p.name) for p in self.stoploss_space}, 13
|
||||
)
|
||||
if HyperoptTools.has_space(self.config, "trailing"):
|
||||
result["trailing"] = round_dict(
|
||||
self.custom_hyperopt.generate_trailing_params(params), 13
|
||||
)
|
||||
if HyperoptTools.has_space(self.config, "trades"):
|
||||
result["max_open_trades"] = round_dict(
|
||||
{
|
||||
"max_open_trades": (
|
||||
self.backtesting.strategy.max_open_trades
|
||||
if self.backtesting.strategy.max_open_trades != float("inf")
|
||||
else -1
|
||||
)
|
||||
},
|
||||
13,
|
||||
)
|
||||
for space in self.spaces.keys():
|
||||
if space == "protection":
|
||||
result["protection"] = round_dict(
|
||||
{p.name: params.get(p.name) for p in self.spaces[space]}, 13
|
||||
)
|
||||
elif space == "roi":
|
||||
result["roi"] = round_dict(
|
||||
{str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items()},
|
||||
13,
|
||||
)
|
||||
elif space == "stoploss":
|
||||
result["stoploss"] = round_dict(
|
||||
{p.name: params.get(p.name) for p in self.spaces[space]}, 13
|
||||
)
|
||||
elif space == "trailing":
|
||||
result["trailing"] = round_dict(
|
||||
self.custom_hyperopt.generate_trailing_params(params), 13
|
||||
)
|
||||
elif space == "trades":
|
||||
result["max_open_trades"] = round_dict(
|
||||
{
|
||||
"max_open_trades": (
|
||||
self.backtesting.strategy.max_open_trades
|
||||
if self.backtesting.strategy.max_open_trades != float("inf")
|
||||
else -1
|
||||
)
|
||||
},
|
||||
13,
|
||||
)
|
||||
else:
|
||||
result[space] = round_dict(
|
||||
{p.name: params.get(p.name) for p in self.spaces[space]}, 13
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -226,56 +222,39 @@ class HyperOptimizer:
|
||||
"""
|
||||
Assign the dimensions in the hyperoptimization space.
|
||||
"""
|
||||
if HyperoptTools.has_space(self.config, "protection"):
|
||||
# Protections can only be optimized when using the Parameter interface
|
||||
logger.debug("Hyperopt has 'protection' space")
|
||||
# Enable Protections if protection space is selected.
|
||||
self.config["enable_protections"] = True
|
||||
self.backtesting.enable_protections = True
|
||||
self.protection_space = self.custom_hyperopt.protection_space()
|
||||
spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "trades"]
|
||||
spaces += [s for s in self.custom_hyperopt.get_available_spaces() if s not in spaces]
|
||||
|
||||
if HyperoptTools.has_space(self.config, "buy"):
|
||||
logger.debug("Hyperopt has 'buy' space")
|
||||
self.buy_space = self.custom_hyperopt.buy_indicator_space()
|
||||
for space in spaces:
|
||||
if not HyperoptTools.has_space(self.config, space):
|
||||
continue
|
||||
logger.debug(f"Hyperopt has '{space}' space")
|
||||
if space == "protection":
|
||||
# Protections can only be optimized when using the Parameter interface
|
||||
# Enable Protections if protection space is selected.
|
||||
self.config["enable_protections"] = True
|
||||
self.backtesting.enable_protections = True
|
||||
self.spaces[space] = self.custom_hyperopt.get_indicator_space(space)
|
||||
elif space == "roi":
|
||||
self.spaces[space] = self.custom_hyperopt.roi_space()
|
||||
elif space == "stoploss":
|
||||
self.spaces[space] = self.custom_hyperopt.stoploss_space()
|
||||
elif space == "trailing":
|
||||
self.spaces[space] = self.custom_hyperopt.trailing_space()
|
||||
elif space == "trades":
|
||||
self.spaces[space] = self.custom_hyperopt.max_open_trades_space()
|
||||
else:
|
||||
self.spaces[space] = self.custom_hyperopt.get_indicator_space(space)
|
||||
|
||||
if HyperoptTools.has_space(self.config, "sell"):
|
||||
logger.debug("Hyperopt has 'sell' space")
|
||||
self.sell_space = self.custom_hyperopt.sell_indicator_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, "roi"):
|
||||
logger.debug("Hyperopt has 'roi' space")
|
||||
self.roi_space = self.custom_hyperopt.roi_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, "stoploss"):
|
||||
logger.debug("Hyperopt has 'stoploss' space")
|
||||
self.stoploss_space = self.custom_hyperopt.stoploss_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, "trailing"):
|
||||
logger.debug("Hyperopt has 'trailing' space")
|
||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, "trades"):
|
||||
logger.debug("Hyperopt has 'trades' space")
|
||||
self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space()
|
||||
|
||||
self.dimensions = (
|
||||
self.buy_space
|
||||
+ self.sell_space
|
||||
+ self.protection_space
|
||||
+ self.roi_space
|
||||
+ self.stoploss_space
|
||||
+ self.trailing_space
|
||||
+ self.max_open_trades_space
|
||||
)
|
||||
|
||||
def assign_params(self, params_dict: dict[str, Any], category: str) -> None:
|
||||
"""
|
||||
Assign hyperoptable parameters
|
||||
"""
|
||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params_dict[attr_name]
|
||||
self.dimensions = [s for space in self.spaces.values() for s in space]
|
||||
if len(self.dimensions) == 0:
|
||||
raise OperationalException(
|
||||
"No hyperopt parameters found to optimize."
|
||||
f"Available spaces: {', '.join(spaces)}. "
|
||||
"Check your strategy's parameter definitions or verify the configured spaces "
|
||||
"in your command."
|
||||
)
|
||||
self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions)
|
||||
|
||||
@delayed
|
||||
@wrap_non_picklable_objects
|
||||
@@ -292,15 +271,9 @@ class HyperOptimizer:
|
||||
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
||||
backtest_start_time = datetime.now(UTC)
|
||||
|
||||
# Apply parameters
|
||||
if HyperoptTools.has_space(self.config, "buy"):
|
||||
self.assign_params(params_dict, "buy")
|
||||
|
||||
if HyperoptTools.has_space(self.config, "sell"):
|
||||
self.assign_params(params_dict, "sell")
|
||||
|
||||
if HyperoptTools.has_space(self.config, "protection"):
|
||||
self.assign_params(params_dict, "protection")
|
||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters():
|
||||
if attr.in_space and attr.optimize:
|
||||
attr.value = params_dict[attr_name]
|
||||
|
||||
if HyperoptTools.has_space(self.config, "roi"):
|
||||
self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(
|
||||
@@ -436,7 +409,6 @@ class HyperOptimizer:
|
||||
o_sampler = self.custom_hyperopt.generate_estimator(
|
||||
dimensions=self.dimensions, random_state=random_state
|
||||
)
|
||||
self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions)
|
||||
|
||||
if isinstance(o_sampler, str):
|
||||
if o_sampler not in optuna_samplers_dict.keys():
|
||||
|
||||
@@ -9,7 +9,7 @@ import numpy as np
|
||||
import rapidjson
|
||||
from pandas import isna, json_normalize
|
||||
|
||||
from freqtrade.constants import FTHYPT_FILEVERSION, Config
|
||||
from freqtrade.constants import FTHYPT_FILEVERSION, HYPEROPT_BUILTIN_SPACES, Config
|
||||
from freqtrade.enums import HyperoptState
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import deep_merge_dicts, round_dict, safe_value_fallback2
|
||||
@@ -219,21 +219,22 @@ class HyperoptTools:
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT))
|
||||
|
||||
else:
|
||||
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, "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
|
||||
)
|
||||
all_spaces = list(params.keys() | non_optimized.keys())
|
||||
# Explicitly listed to keep original sort order
|
||||
spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "max_open_trades"]
|
||||
spaces += [s for s in all_spaces if s not in spaces]
|
||||
lookup = {
|
||||
"roi": "ROI",
|
||||
"trailing": "Trailing stop",
|
||||
}
|
||||
for space in spaces:
|
||||
name = lookup.get(
|
||||
space, space.capitalize() if space in HYPEROPT_BUILTIN_SPACES else space
|
||||
)
|
||||
|
||||
HyperoptTools._params_pretty_print(
|
||||
params, space, f"{name} parameters:", non_optimized
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
||||
|
||||
@@ -4,9 +4,9 @@ This module defines a base class for auto-hyperoptable strategies.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
@@ -18,6 +18,11 @@ from freqtrade.strategy.parameters import BaseParameter
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Type aliases
|
||||
SpaceParams = dict[str, BaseParameter]
|
||||
AllSpaceParams = dict[str, SpaceParams]
|
||||
|
||||
|
||||
class HyperStrategyMixin:
|
||||
"""
|
||||
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
||||
@@ -29,9 +34,7 @@ class HyperStrategyMixin:
|
||||
Initialize hyperoptable strategy mixin.
|
||||
"""
|
||||
self.config = config
|
||||
self.ft_buy_params: list[BaseParameter] = []
|
||||
self.ft_sell_params: list[BaseParameter] = []
|
||||
self.ft_protection_params: list[BaseParameter] = []
|
||||
self._ft_hyper_params: AllSpaceParams = {}
|
||||
|
||||
params = self.load_params_from_file()
|
||||
params = params.get("params", {})
|
||||
@@ -46,30 +49,9 @@ class HyperStrategyMixin:
|
||||
:param category:
|
||||
:return:
|
||||
"""
|
||||
if category not in ("buy", "sell", "protection", None):
|
||||
raise OperationalException(
|
||||
'Category must be one of: "buy", "sell", "protection", None.'
|
||||
)
|
||||
|
||||
if category is None:
|
||||
params = self.ft_buy_params + self.ft_sell_params + self.ft_protection_params
|
||||
else:
|
||||
params = getattr(self, f"ft_{category}_params")
|
||||
|
||||
for par in params:
|
||||
yield par.name, par
|
||||
|
||||
@classmethod
|
||||
def detect_all_parameters(cls) -> dict:
|
||||
"""Detect all parameters and return them as a list"""
|
||||
params: dict[str, Any] = {
|
||||
"buy": list(detect_parameters(cls, "buy")),
|
||||
"sell": list(detect_parameters(cls, "sell")),
|
||||
"protection": list(detect_parameters(cls, "protection")),
|
||||
}
|
||||
params.update({"count": len(params["buy"] + params["sell"] + params["protection"])})
|
||||
|
||||
return params
|
||||
for category in [c for c in self._ft_hyper_params if category is None or c == category]:
|
||||
for par in self._ft_hyper_params[category].values():
|
||||
yield par.name, par
|
||||
|
||||
def ft_load_params_from_file(self) -> None:
|
||||
"""
|
||||
@@ -110,20 +92,13 @@ class HyperStrategyMixin:
|
||||
* Parameters defined in parameters objects (buy_params, sell_params, ...)
|
||||
* Parameter defaults
|
||||
"""
|
||||
self._ft_hyper_params = detect_all_parameters(self)
|
||||
|
||||
buy_params = deep_merge_dicts(
|
||||
self._ft_params_from_file.get("buy", {}), getattr(self, "buy_params", {})
|
||||
)
|
||||
sell_params = deep_merge_dicts(
|
||||
self._ft_params_from_file.get("sell", {}), getattr(self, "sell_params", {})
|
||||
)
|
||||
protection_params = deep_merge_dicts(
|
||||
self._ft_params_from_file.get("protection", {}), getattr(self, "protection_params", {})
|
||||
)
|
||||
|
||||
self._ft_load_params(buy_params, "buy", hyperopt)
|
||||
self._ft_load_params(sell_params, "sell", hyperopt)
|
||||
self._ft_load_params(protection_params, "protection", hyperopt)
|
||||
for space in self._ft_hyper_params.keys():
|
||||
params_values = deep_merge_dicts(
|
||||
self._ft_params_from_file.get(space, {}), getattr(self, f"{space}_params", {})
|
||||
)
|
||||
self._ft_load_params(self._ft_hyper_params[space], params_values, space, hyperopt)
|
||||
|
||||
def load_params_from_file(self) -> dict:
|
||||
filename_str = getattr(self, "__file__", "")
|
||||
@@ -145,72 +120,73 @@ class HyperStrategyMixin:
|
||||
|
||||
return {}
|
||||
|
||||
def _ft_load_params(self, params: dict, space: str, hyperopt: bool = False) -> None:
|
||||
def _ft_load_params(
|
||||
self, params: SpaceParams, param_values: dict, space: str, hyperopt: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Set optimizable parameter values.
|
||||
:param params: Dictionary with new parameter values.
|
||||
"""
|
||||
if not params:
|
||||
if not param_values:
|
||||
logger.info(f"No params for {space} found, using default values.")
|
||||
param_container: list[BaseParameter] = getattr(self, f"ft_{space}_params")
|
||||
|
||||
for attr_name, attr in detect_parameters(self, space):
|
||||
attr.name = attr_name
|
||||
attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space)
|
||||
if not attr.category:
|
||||
attr.category = space
|
||||
for param_name, param in params.items():
|
||||
param.in_space = hyperopt and HyperoptTools.has_space(self.config, space)
|
||||
if not param.category:
|
||||
param.category = space
|
||||
|
||||
param_container.append(attr)
|
||||
|
||||
if params and attr_name in params:
|
||||
if attr.load:
|
||||
attr.value = params[attr_name]
|
||||
logger.info(f"Strategy Parameter: {attr_name} = {attr.value}")
|
||||
if param_values and param_name in param_values:
|
||||
if param.load:
|
||||
param.value = param_values[param_name]
|
||||
logger.info(f"Strategy Parameter: {param_name} = {param.value}")
|
||||
else:
|
||||
logger.warning(
|
||||
f'Parameter "{attr_name}" exists, but is disabled. '
|
||||
f'Default value "{attr.value}" used.'
|
||||
f'Parameter "{param_name}" exists, but is disabled. '
|
||||
f'Default value "{param.value}" used.'
|
||||
)
|
||||
else:
|
||||
logger.info(f"Strategy Parameter(default): {attr_name} = {attr.value}")
|
||||
logger.info(f"Strategy Parameter(default): {param_name} = {param.value}")
|
||||
|
||||
def get_no_optimize_params(self) -> dict[str, dict]:
|
||||
"""
|
||||
Returns list of Parameters that are not part of the current optimize job
|
||||
"""
|
||||
params: dict[str, dict] = {
|
||||
"buy": {},
|
||||
"sell": {},
|
||||
"protection": {},
|
||||
}
|
||||
params: dict[str, dict] = defaultdict(dict)
|
||||
for name, p in self.enumerate_parameters():
|
||||
if p.category and (not p.optimize or not p.in_space):
|
||||
params[p.category][name] = p.value
|
||||
return params
|
||||
|
||||
|
||||
def detect_parameters(
|
||||
obj: HyperStrategyMixin | type[HyperStrategyMixin], category: str
|
||||
) -> Iterator[tuple[str, BaseParameter]]:
|
||||
def detect_all_parameters(
|
||||
obj: HyperStrategyMixin | type[HyperStrategyMixin],
|
||||
) -> AllSpaceParams:
|
||||
"""
|
||||
Detect all parameters for 'category' for "obj"
|
||||
Detect all hyperoptable parameters for this object.
|
||||
:param obj: Strategy object or class
|
||||
:param category: category - usually `'buy', 'sell', 'protection',...
|
||||
:return: Dictionary of detected parameters by space
|
||||
"""
|
||||
auto_categories = ["buy", "sell", "enter", "exit", "protection"]
|
||||
result: AllSpaceParams = defaultdict(dict)
|
||||
for attr_name in dir(obj):
|
||||
if not attr_name.startswith("__"): # Ignore internals, not strictly necessary.
|
||||
attr = getattr(obj, attr_name)
|
||||
if issubclass(attr.__class__, BaseParameter):
|
||||
if (
|
||||
attr_name.startswith(category + "_")
|
||||
and attr.category is not None
|
||||
and attr.category != category
|
||||
):
|
||||
raise OperationalException(
|
||||
f"Inconclusive parameter name {attr_name}, category: {attr.category}."
|
||||
)
|
||||
if attr_name.startswith("__"): # Ignore internals
|
||||
continue
|
||||
attr = getattr(obj, attr_name)
|
||||
if not issubclass(attr.__class__, BaseParameter):
|
||||
continue
|
||||
if not attr.category:
|
||||
# Category auto detection
|
||||
for category in auto_categories:
|
||||
if attr_name.startswith(category + "_"):
|
||||
attr.category = category
|
||||
break
|
||||
if attr.category is None:
|
||||
raise OperationalException(f"Cannot determine parameter space for {attr_name}.")
|
||||
|
||||
if category == attr.category or (
|
||||
attr_name.startswith(category + "_") and attr.category is None
|
||||
):
|
||||
yield attr_name, attr
|
||||
if attr.category in ("all", "default") or attr.category.isidentifier() is False:
|
||||
raise OperationalException(
|
||||
f"'{attr.category}' is not a valid space. Parameter: {attr_name}."
|
||||
)
|
||||
attr.name = attr_name
|
||||
result[attr.category][attr_name] = attr
|
||||
return result
|
||||
|
||||
@@ -49,9 +49,9 @@ class BaseParameter(ABC):
|
||||
):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param space: The parameter space. Can be 'buy', 'sell', or a string that's also a
|
||||
valid python identifier.
|
||||
This parameter is optional if parameter name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to optuna.distributions.
|
||||
@@ -109,8 +109,9 @@ class NumericParameter(BaseParameter):
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param space: The parameter space. Can be 'buy', 'sell', or a string that's also a
|
||||
valid python identifier.
|
||||
This parameter is optional if parameter name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to optuna.distributions.*.
|
||||
@@ -151,8 +152,9 @@ class IntParameter(NumericParameter):
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param space: The parameter space. Can be 'buy', 'sell', or a string that's also a
|
||||
valid python identifier.
|
||||
This parameter is optional if parameter name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to optuna.distributions.IntDistribution.
|
||||
@@ -205,8 +207,9 @@ class RealParameter(NumericParameter):
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param space: The parameter space. Can be 'buy', 'sell', or a string that's also a
|
||||
valid python identifier.
|
||||
This parameter is optional if parameter name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to optuna.distributions.FloatDistribution.
|
||||
@@ -245,8 +248,9 @@ class DecimalParameter(NumericParameter):
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param decimals: A number of decimals after floating point to be included in testing.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param space: The parameter space. Can be 'buy', 'sell', or a string that's also a
|
||||
valid python identifier.
|
||||
This parameter is optional if parameter name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to optuna's NumericParameter.
|
||||
@@ -310,10 +314,10 @@ class CategoricalParameter(BaseParameter):
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param categories: Optimization space, [a, b, ...].
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
used.
|
||||
:param space: The parameter space. Can be 'buy', 'sell', or a string that's also a
|
||||
valid python identifier.
|
||||
This parameter is optional if parameter name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Compatibility. Optuna's CategoricalDistribution does not
|
||||
@@ -361,10 +365,10 @@ class BooleanParameter(CategoricalParameter):
|
||||
Initialize hyperopt-optimizable Boolean Parameter.
|
||||
It's a shortcut to `CategoricalParameter([True, False])`.
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
used.
|
||||
:param space: The parameter space. Can be 'buy', 'sell', or a string that's also a
|
||||
valid python identifier.
|
||||
This parameter is optional if parameter name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to optuna.distributions.CategoricalDistribution.
|
||||
|
||||
@@ -96,7 +96,9 @@ class SampleStrategy(IStrategy):
|
||||
buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
|
||||
sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True)
|
||||
short_rsi = IntParameter(low=51, high=100, default=70, space="sell", optimize=True, load=True)
|
||||
exit_short_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
|
||||
exit_short_rsi = IntParameter(
|
||||
low=1, high=50, default=30, space="exit", optimize=True, load=True
|
||||
)
|
||||
|
||||
# Number of candles the strategy requires before producing valid signals
|
||||
startup_candle_count: int = 200
|
||||
|
||||
Reference in New Issue
Block a user