freqtrade_origin/freqtrade/resolvers/strategy_resolver.py

320 lines
13 KiB
Python
Raw Normal View History

# pragma pylint: disable=attribute-defined-outside-init
2018-01-28 05:26:57 +00:00
"""
This module load custom strategies
"""
2024-05-12 14:21:12 +00:00
2018-03-25 19:37:14 +00:00
import logging
2018-07-30 20:00:08 +00:00
import tempfile
from base64 import urlsafe_b64decode
2019-01-21 18:30:59 +00:00
from inspect import getfullargspec
from os import walk
2018-07-30 20:00:08 +00:00
from pathlib import Path
2022-09-18 11:31:52 +00:00
from typing import Any, List, Optional
2018-03-17 21:44:47 +00:00
from freqtrade.configuration.config_validation import validate_migrated_strategy_settings
2022-09-18 11:31:52 +00:00
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES, Config
from freqtrade.enums import TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import IResolver
2018-01-15 08:35:11 +00:00
from freqtrade.strategy.interface import IStrategy
2020-09-28 17:39:41 +00:00
2018-03-25 19:37:14 +00:00
logger = logging.getLogger(__name__)
2018-03-24 20:56:20 +00:00
class StrategyResolver(IResolver):
2018-01-28 05:26:57 +00:00
"""
This class contains the logic to load custom strategy class
2018-01-28 05:26:57 +00:00
"""
2024-05-12 14:21:12 +00:00
2019-12-24 12:34:37 +00:00
object_type = IStrategy
2019-12-24 12:54:46 +00:00
object_type_str = "Strategy"
2020-02-02 15:12:23 +00:00
user_subdir = USERPATH_STRATEGIES
initial_search_path = None
extra_path = "strategy_path"
@staticmethod
2023-01-21 14:01:56 +00:00
def load_strategy(config: Optional[Config] = None) -> IStrategy:
2018-01-28 05:26:57 +00:00
"""
Load the custom class from config parameter
2018-03-25 18:24:56 +00:00
:param config: configuration dictionary or None
2018-01-28 05:26:57 +00:00
"""
2018-03-24 17:14:05 +00:00
config = config or {}
2024-05-12 14:21:12 +00:00
if not config.get("strategy"):
raise OperationalException(
2024-05-12 15:51:21 +00:00
"No strategy set. Please use `--strategy` to specify the strategy class to use."
2024-05-12 14:21:12 +00:00
)
2019-09-21 17:54:44 +00:00
2024-05-12 14:21:12 +00:00
strategy_name = config["strategy"]
strategy: IStrategy = StrategyResolver._load_strategy(
2024-05-12 14:21:12 +00:00
strategy_name, config=config, extra_dir=config.get("strategy_path")
)
2022-05-30 04:52:44 +00:00
strategy.ft_load_params_from_file()
2018-01-15 08:35:11 +00:00
# Set attributes
# Check if we need to override configuration
# (Attribute name, default, subkey)
2024-05-12 14:21:12 +00:00
attributes = [
("minimal_roi", {"0": 10.0}),
("timeframe", None),
("stoploss", None),
("trailing_stop", None),
("trailing_stop_positive", None),
("trailing_stop_positive_offset", 0.0),
("trailing_only_offset_is_reached", None),
("use_custom_stoploss", None),
("process_only_new_candles", None),
("order_types", None),
("order_time_in_force", None),
("stake_currency", None),
("stake_amount", None),
("protections", None),
("startup_candle_count", None),
("unfilledtimeout", None),
("use_exit_signal", True),
("exit_profit_only", False),
("ignore_roi_if_entry_signal", False),
("exit_profit_offset", 0.0),
("disable_dataframe_checks", False),
("ignore_buying_expired_candle_after", 0),
("position_adjustment_enable", False),
("max_entry_position_adjustment", -1),
("max_open_trades", -1),
]
2021-06-26 15:47:41 +00:00
for attribute, default in attributes:
2024-05-12 14:21:12 +00:00
StrategyResolver._override_attribute_helper(strategy, config, attribute, default)
2019-01-05 06:25:35 +00:00
# Loop this list again to have output combined
2021-06-26 15:47:41 +00:00
for attribute, _ in attributes:
if attribute in config:
2019-01-05 06:24:15 +00:00
logger.info("Strategy using %s: %s", attribute, config[attribute])
StrategyResolver._normalize_attributes(strategy)
2018-08-09 17:24:00 +00:00
StrategyResolver._strategy_sanity_validations(strategy)
return strategy
@staticmethod
2022-09-18 11:31:52 +00:00
def _override_attribute_helper(strategy, config: Config, attribute: str, default: Any):
"""
Override attributes in the strategy.
Prevalence:
- Configuration
- Strategy
- default (if not None)
"""
2024-05-12 14:21:12 +00:00
if attribute in config and not isinstance(
getattr(type(strategy), attribute, None), property
):
# Ensure Properties are not overwritten
setattr(strategy, attribute, config[attribute])
2024-05-12 14:21:12 +00:00
logger.info(
"Override strategy '%s' with value in config file: %s.",
attribute,
config[attribute],
)
elif hasattr(strategy, attribute):
val = getattr(strategy, attribute)
# None's cannot exist in the config, so do not copy them
if val is not None:
# max_open_trades set to -1 in the strategy will be copied as infinity in the config
2024-05-12 14:21:12 +00:00
if attribute == "max_open_trades" and val == -1:
config[attribute] = float("inf")
else:
config[attribute] = val
# Explicitly check for None here as other "falsy" values are possible
elif default is not None:
setattr(strategy, attribute, default)
config[attribute] = default
2018-11-25 21:02:59 +00:00
@staticmethod
def _normalize_attributes(strategy: IStrategy) -> IStrategy:
"""
Normalize attributes to have the correct type.
"""
# Sort and apply type conversions
2024-05-12 14:21:12 +00:00
if hasattr(strategy, "minimal_roi"):
strategy.minimal_roi = dict(
sorted(
{int(key): value for (key, value) in strategy.minimal_roi.items()}.items(),
key=lambda t: t[0],
)
)
if hasattr(strategy, "stoploss"):
strategy.stoploss = float(strategy.stoploss)
2024-05-12 14:21:12 +00:00
if hasattr(strategy, "max_open_trades") and strategy.max_open_trades < 0:
strategy.max_open_trades = float("inf")
return strategy
@staticmethod
def _strategy_sanity_validations(strategy: IStrategy):
# Ensure necessary migrations are performed first.
validate_migrated_strategy_settings(strategy.config)
2019-12-24 12:54:46 +00:00
if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):
2024-05-12 14:21:12 +00:00
raise ImportError(
f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-types mapping is incomplete."
)
2019-12-24 12:54:46 +00:00
if not all(k in strategy.order_time_in_force for k in REQUIRED_ORDERTIF):
2024-05-12 14:21:12 +00:00
raise ImportError(
f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-time-in-force mapping is incomplete."
)
trading_mode = strategy.config.get("trading_mode", TradingMode.SPOT)
2024-05-12 14:21:12 +00:00
if strategy.can_short and trading_mode == TradingMode.SPOT:
raise ImportError(
"Short strategies cannot run in spot markets. Please make sure that this "
"is the correct strategy and that your trading mode configuration is correct. "
2022-03-11 18:43:00 +00:00
"You can run this strategy in spot markets by setting `can_short=False`"
" in your strategy. Please note that short signals will be ignored in that case."
2024-05-12 14:21:12 +00:00
)
2018-11-25 21:02:59 +00:00
@staticmethod
def validate_strategy(strategy: IStrategy) -> IStrategy:
2024-05-12 14:21:12 +00:00
if strategy.config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
# Require new method
2024-05-12 14:21:12 +00:00
warn_deprecated_setting(strategy, "sell_profit_only", "exit_profit_only", True)
warn_deprecated_setting(strategy, "sell_profit_offset", "exit_profit_offset", True)
warn_deprecated_setting(strategy, "use_sell_signal", "use_exit_signal", True)
warn_deprecated_setting(
strategy, "ignore_roi_if_buy_signal", "ignore_roi_if_entry_signal", True
)
if not check_override(strategy, IStrategy, "populate_entry_trend"):
raise OperationalException("`populate_entry_trend` must be implemented.")
2024-05-12 14:21:12 +00:00
if not check_override(strategy, IStrategy, "populate_exit_trend"):
raise OperationalException("`populate_exit_trend` must be implemented.")
2024-05-12 14:21:12 +00:00
if check_override(strategy, IStrategy, "check_buy_timeout"):
raise OperationalException(
"Please migrate your implementation "
"of `check_buy_timeout` to `check_entry_timeout`."
)
if check_override(strategy, IStrategy, "check_sell_timeout"):
raise OperationalException(
2024-05-12 14:21:12 +00:00
"Please migrate your implementation "
"of `check_sell_timeout` to `check_exit_timeout`."
)
if check_override(strategy, IStrategy, "custom_sell"):
raise OperationalException(
"Please migrate your implementation of `custom_sell` to `custom_exit`."
)
2022-04-05 18:43:39 +00:00
else:
# TODO: Implementing one of the following methods should show a deprecation warning
# buy_trend and sell_trend, custom_sell
2024-05-12 14:21:12 +00:00
warn_deprecated_setting(strategy, "sell_profit_only", "exit_profit_only")
warn_deprecated_setting(strategy, "sell_profit_offset", "exit_profit_offset")
warn_deprecated_setting(strategy, "use_sell_signal", "use_exit_signal")
warn_deprecated_setting(
strategy, "ignore_roi_if_buy_signal", "ignore_roi_if_entry_signal"
)
if not check_override(strategy, IStrategy, "populate_buy_trend") and not check_override(
strategy, IStrategy, "populate_entry_trend"
):
raise OperationalException(
2024-05-12 14:21:12 +00:00
"`populate_entry_trend` or `populate_buy_trend` must be implemented."
)
if not check_override(
strategy, IStrategy, "populate_sell_trend"
) and not check_override(strategy, IStrategy, "populate_exit_trend"):
raise OperationalException(
2024-05-12 14:21:12 +00:00
"`populate_exit_trend` or `populate_sell_trend` must be implemented."
)
2022-04-25 05:01:27 +00:00
_populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
_buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
_sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
2024-05-12 14:21:12 +00:00
if any(x == 2 for x in [_populate_fun_len, _buy_fun_len, _sell_fun_len]):
2022-04-25 05:01:27 +00:00
raise OperationalException(
"Strategy Interface v1 is no longer supported. "
"Please update your strategy to implement "
"`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` "
2024-05-12 14:21:12 +00:00
"with the metadata argument. "
)
2024-05-12 14:21:12 +00:00
has_after_fill = "after_fill" in getfullargspec(
strategy.custom_stoploss
).args and check_override(strategy, IStrategy, "custom_stoploss")
if has_after_fill:
strategy._ft_stop_uses_after_fill = True
return strategy
2018-11-25 21:02:59 +00:00
@staticmethod
2024-05-12 14:21:12 +00:00
def _load_strategy(
strategy_name: str, config: Config, extra_dir: Optional[str] = None
) -> IStrategy:
2018-01-15 08:35:11 +00:00
"""
Search and loads the specified strategy.
2018-01-15 08:35:11 +00:00
:param strategy_name: name of the module to import
:param config: configuration for the strategy
2018-03-25 14:28:04 +00:00
:param extra_dir: additional directory to search for the given strategy
:return: Strategy instance or None
2018-01-15 08:35:11 +00:00
"""
2024-05-12 14:21:12 +00:00
if config.get("recursive_strategy_search", False):
extra_dirs: List[str] = [
path[0] for path in walk(f"{config['user_data_dir']}/{USERPATH_STRATEGIES}")
] # sub-directories
else:
extra_dirs = []
if extra_dir:
extra_dirs.append(extra_dir)
2024-05-12 14:21:12 +00:00
abs_paths = StrategyResolver.build_search_paths(
config, user_subdir=USERPATH_STRATEGIES, extra_dirs=extra_dirs
)
2018-03-25 14:28:04 +00:00
2018-07-05 21:40:04 +00:00
if ":" in strategy_name:
2019-06-26 19:23:16 +00:00
logger.info("loading base64 encoded strategy")
strat = strategy_name.split(":")
if len(strat) == 2:
temp = Path(tempfile.mkdtemp("freq", "strategy"))
name = strat[0] + ".py"
2024-05-12 14:21:12 +00:00
temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode("utf-8"))
temp.joinpath("__init__.py").touch()
2018-11-24 19:39:16 +00:00
strategy_name = strat[0]
# register temp path with the bot
2018-11-24 19:39:16 +00:00
abs_paths.insert(0, temp.resolve())
2021-08-18 12:03:44 +00:00
strategy = StrategyResolver._load_object(
paths=abs_paths,
object_name=strategy_name,
add_source=True,
2024-05-12 14:21:12 +00:00
kwargs={"config": config},
2021-08-18 12:03:44 +00:00
)
if strategy:
return StrategyResolver.validate_strategy(strategy)
2019-07-12 20:45:49 +00:00
raise OperationalException(
f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
"or contains Python code errors."
2018-03-25 14:28:04 +00:00
)
2022-04-05 18:07:58 +00:00
def warn_deprecated_setting(strategy: IStrategy, old: str, new: str, error=False):
if hasattr(strategy, old):
errormsg = f"DEPRECATED: Using '{old}' moved to '{new}'."
if error:
raise OperationalException(errormsg)
logger.warning(errormsg)
2024-05-12 14:21:12 +00:00
setattr(strategy, new, getattr(strategy, f"{old}"))
def check_override(object, parentclass, attribute):
"""
Checks if a object overrides the parent class attribute.
2022-03-12 10:15:27 +00:00
:returns: True if the object is overridden.
"""
2022-03-12 10:15:27 +00:00
return getattr(type(object), attribute) != getattr(parentclass, attribute)