2018-01-28 21:21:25 +00:00
|
|
|
# pragma pylint: disable=attribute-defined-outside-init
|
|
|
|
|
2018-01-28 05:26:57 +00:00
|
|
|
"""
|
|
|
|
This module load custom strategies
|
|
|
|
"""
|
2018-03-25 19:37:14 +00:00
|
|
|
import logging
|
2018-07-30 20:00:08 +00:00
|
|
|
import tempfile
|
2018-07-05 21:30:24 +00:00
|
|
|
from base64 import urlsafe_b64decode
|
2019-01-21 18:30:59 +00:00
|
|
|
from inspect import getfullargspec
|
2018-07-30 20:00:08 +00:00
|
|
|
from pathlib import Path
|
2020-02-02 04:00:40 +00:00
|
|
|
from typing import Any, Dict, Optional
|
2018-03-17 21:44:47 +00:00
|
|
|
|
2022-03-07 06:09:01 +00:00
|
|
|
from freqtrade.configuration.config_validation import validate_migrated_strategy_settings
|
2020-09-28 17:39:41 +00:00
|
|
|
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES
|
2022-03-11 05:59:28 +00:00
|
|
|
from freqtrade.enums import TradingMode
|
2019-12-30 14:02:17 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
2018-11-24 19:00:02 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
2018-11-24 19:00:02 +00:00
|
|
|
class StrategyResolver(IResolver):
|
2018-01-28 05:26:57 +00:00
|
|
|
"""
|
2019-12-23 09:23:48 +00:00
|
|
|
This class contains the logic to load custom strategy class
|
2018-01-28 05:26:57 +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
|
2020-02-18 19:26:20 +00:00
|
|
|
initial_search_path = None
|
2018-03-24 22:32:17 +00:00
|
|
|
|
2019-12-23 09:23:48 +00:00
|
|
|
@staticmethod
|
2020-02-02 04:00:40 +00:00
|
|
|
def load_strategy(config: Dict[str, Any] = 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 {}
|
|
|
|
|
2019-09-21 17:54:44 +00:00
|
|
|
if not config.get('strategy'):
|
|
|
|
raise OperationalException("No strategy set. Please use `--strategy` to specify "
|
|
|
|
"the strategy class to use.")
|
|
|
|
|
|
|
|
strategy_name = config['strategy']
|
2019-12-23 09:23:48 +00:00
|
|
|
strategy: IStrategy = StrategyResolver._load_strategy(
|
|
|
|
strategy_name, config=config,
|
|
|
|
extra_dir=config.get('strategy_path'))
|
2019-01-13 18:28:20 +00:00
|
|
|
|
2020-06-02 07:50:56 +00:00
|
|
|
if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
|
|
|
|
# Assign ticker_interval to timeframe to keep compatibility
|
|
|
|
if 'timeframe' not in config:
|
|
|
|
logger.warning(
|
2020-06-15 04:35:31 +00:00
|
|
|
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
|
2021-08-06 22:19:36 +00:00
|
|
|
)
|
2020-06-02 07:50:56 +00:00
|
|
|
strategy.timeframe = strategy.ticker_interval
|
|
|
|
|
2021-06-29 05:07:34 +00:00
|
|
|
if strategy._ft_params_from_file:
|
|
|
|
# Set parameters from Hyperopt results file
|
|
|
|
params = strategy._ft_params_from_file
|
2021-10-27 04:29:35 +00:00
|
|
|
strategy.minimal_roi = params.get('roi', getattr(strategy, 'minimal_roi', {}))
|
2021-06-29 05:07:34 +00:00
|
|
|
|
2021-10-27 04:29:35 +00:00
|
|
|
strategy.stoploss = params.get('stoploss', {}).get(
|
|
|
|
'stoploss', getattr(strategy, 'stoploss', -0.1))
|
2021-06-29 05:07:34 +00:00
|
|
|
trailing = params.get('trailing', {})
|
2021-10-27 04:29:35 +00:00
|
|
|
strategy.trailing_stop = trailing.get(
|
|
|
|
'trailing_stop', getattr(strategy, 'trailing_stop', False))
|
|
|
|
strategy.trailing_stop_positive = trailing.get(
|
|
|
|
'trailing_stop_positive', getattr(strategy, 'trailing_stop_positive', None))
|
2021-06-29 05:07:34 +00:00
|
|
|
strategy.trailing_stop_positive_offset = trailing.get(
|
2021-10-27 04:29:35 +00:00
|
|
|
'trailing_stop_positive_offset',
|
|
|
|
getattr(strategy, 'trailing_stop_positive_offset', 0))
|
2021-06-29 05:07:34 +00:00
|
|
|
strategy.trailing_only_offset_is_reached = trailing.get(
|
2021-10-27 04:29:35 +00:00
|
|
|
'trailing_only_offset_is_reached',
|
|
|
|
getattr(strategy, 'trailing_only_offset_is_reached', 0.0))
|
2021-06-29 05:07:34 +00:00
|
|
|
|
2018-01-15 08:35:11 +00:00
|
|
|
# Set attributes
|
|
|
|
# Check if we need to override configuration
|
2020-05-29 17:37:18 +00:00
|
|
|
# (Attribute name, default, subkey)
|
2021-06-26 15:47:41 +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_sell_signal", True),
|
|
|
|
("sell_profit_only", False),
|
|
|
|
("ignore_roi_if_buy_signal", False),
|
|
|
|
("sell_profit_offset", 0.0),
|
|
|
|
("disable_dataframe_checks", False),
|
2021-12-24 10:38:43 +00:00
|
|
|
("ignore_buying_expired_candle_after", 0),
|
2022-01-19 07:50:13 +00:00
|
|
|
("position_adjustment_enable", False),
|
2022-01-27 15:57:50 +00:00
|
|
|
("max_entry_position_adjustment", -1),
|
2019-01-05 06:22:19 +00:00
|
|
|
]
|
2021-06-26 15:47:41 +00:00
|
|
|
for attribute, default in attributes:
|
|
|
|
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])
|
|
|
|
|
2020-12-09 06:52:58 +00:00
|
|
|
StrategyResolver._normalize_attributes(strategy)
|
2018-08-09 17:24:00 +00:00
|
|
|
|
2019-12-23 09:23:48 +00:00
|
|
|
StrategyResolver._strategy_sanity_validations(strategy)
|
|
|
|
return strategy
|
2018-11-17 09:14:18 +00:00
|
|
|
|
2019-12-23 09:23:48 +00:00
|
|
|
@staticmethod
|
2020-02-02 04:00:40 +00:00
|
|
|
def _override_attribute_helper(strategy, config: Dict[str, Any],
|
|
|
|
attribute: str, default: Any):
|
2019-01-13 18:28:20 +00:00
|
|
|
"""
|
|
|
|
Override attributes in the strategy.
|
|
|
|
Prevalence:
|
|
|
|
- Configuration
|
|
|
|
- Strategy
|
|
|
|
- default (if not None)
|
|
|
|
"""
|
2021-06-17 19:01:22 +00:00
|
|
|
if (attribute in config
|
2021-08-04 18:10:32 +00:00
|
|
|
and not isinstance(getattr(type(strategy), attribute, None), property)):
|
2021-06-17 19:01:22 +00:00
|
|
|
# Ensure Properties are not overwritten
|
2019-12-23 09:23:48 +00:00
|
|
|
setattr(strategy, attribute, config[attribute])
|
2019-01-05 06:20:38 +00:00
|
|
|
logger.info("Override strategy '%s' with value in config file: %s.",
|
|
|
|
attribute, config[attribute])
|
2019-12-23 09:23:48 +00:00
|
|
|
elif hasattr(strategy, attribute):
|
|
|
|
val = getattr(strategy, attribute)
|
2019-10-11 19:59:13 +00:00
|
|
|
# None's cannot exist in the config, so do not copy them
|
|
|
|
if val is not None:
|
|
|
|
config[attribute] = val
|
2019-01-13 18:28:20 +00:00
|
|
|
# Explicitly check for None here as other "falsy" values are possible
|
|
|
|
elif default is not None:
|
2019-12-23 09:23:48 +00:00
|
|
|
setattr(strategy, attribute, default)
|
2019-01-13 18:28:20 +00:00
|
|
|
config[attribute] = default
|
2018-11-25 21:02:59 +00:00
|
|
|
|
2020-12-09 06:52:58 +00:00
|
|
|
@staticmethod
|
|
|
|
def _normalize_attributes(strategy: IStrategy) -> IStrategy:
|
|
|
|
"""
|
|
|
|
Normalize attributes to have the correct type.
|
|
|
|
"""
|
|
|
|
# Assign deprecated variable - to not break users code relying on this.
|
|
|
|
if hasattr(strategy, 'timeframe'):
|
|
|
|
strategy.ticker_interval = strategy.timeframe
|
|
|
|
|
|
|
|
# Sort and apply type conversions
|
|
|
|
if hasattr(strategy, 'minimal_roi'):
|
2021-06-13 09:45:23 +00:00
|
|
|
strategy.minimal_roi = dict(sorted(
|
2020-12-09 06:52:58 +00:00
|
|
|
{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)
|
|
|
|
return strategy
|
|
|
|
|
2019-12-23 09:23:48 +00:00
|
|
|
@staticmethod
|
2022-03-11 05:59:28 +00:00
|
|
|
def _strategy_sanity_validations(strategy: IStrategy):
|
2022-03-07 06:09:01 +00:00
|
|
|
# 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):
|
2019-12-23 09:23:48 +00:00
|
|
|
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
|
2018-11-17 11:59:16 +00:00
|
|
|
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):
|
2019-12-23 09:23:48 +00:00
|
|
|
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
|
2018-11-25 21:02:59 +00:00
|
|
|
f"Order-time-in-force mapping is incomplete.")
|
2022-03-11 05:59:28 +00:00
|
|
|
trading_mode = strategy.config.get('trading_mode', TradingMode.SPOT)
|
|
|
|
|
|
|
|
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."
|
2022-03-11 05:59:28 +00:00
|
|
|
)
|
2018-11-25 21:02:59 +00:00
|
|
|
|
2019-12-23 09:23:48 +00:00
|
|
|
@staticmethod
|
|
|
|
def _load_strategy(strategy_name: str,
|
|
|
|
config: dict, extra_dir: Optional[str] = None) -> IStrategy:
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
2018-03-24 19:44:04 +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
|
2018-07-16 05:11:17 +00:00
|
|
|
: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
|
2018-03-24 22:20:21 +00:00
|
|
|
:return: Strategy instance or None
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
2018-11-24 19:00:02 +00:00
|
|
|
|
2019-12-24 12:54:46 +00:00
|
|
|
abs_paths = StrategyResolver.build_search_paths(config,
|
2020-02-02 15:12:23 +00:00
|
|
|
user_subdir=USERPATH_STRATEGIES,
|
2019-12-24 12:34:37 +00:00
|
|
|
extra_dir=extra_dir)
|
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")
|
2018-07-05 21:30:24 +00:00
|
|
|
strat = strategy_name.split(":")
|
|
|
|
|
|
|
|
if len(strat) == 2:
|
|
|
|
temp = Path(tempfile.mkdtemp("freq", "strategy"))
|
|
|
|
name = strat[0] + ".py"
|
|
|
|
|
|
|
|
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]
|
2018-07-05 21:30:24 +00:00
|
|
|
|
|
|
|
# register temp path with the bot
|
2018-11-24 19:39:16 +00:00
|
|
|
abs_paths.insert(0, temp.resolve())
|
2018-07-05 21:30:24 +00:00
|
|
|
|
2021-08-18 12:03:44 +00:00
|
|
|
strategy = StrategyResolver._load_object(
|
|
|
|
paths=abs_paths,
|
|
|
|
object_name=strategy_name,
|
|
|
|
add_source=True,
|
|
|
|
kwargs={'config': config},
|
|
|
|
)
|
|
|
|
|
2019-07-21 12:52:59 +00:00
|
|
|
if strategy:
|
2022-03-12 08:31:14 +00:00
|
|
|
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
|
|
|
# Require new method
|
2022-03-12 10:15:27 +00:00
|
|
|
if not check_override(strategy, IStrategy, 'populate_entry_trend'):
|
2022-03-12 08:31:14 +00:00
|
|
|
raise OperationalException("`populate_entry_trend` must be implemented.")
|
2022-03-12 10:15:27 +00:00
|
|
|
if not check_override(strategy, IStrategy, 'populate_exit_trend'):
|
2022-03-12 08:31:14 +00:00
|
|
|
raise OperationalException("`populate_exit_trend` must be implemented.")
|
2022-03-12 10:15:27 +00:00
|
|
|
if check_override(strategy, IStrategy, 'custom_sell'):
|
|
|
|
raise OperationalException(
|
|
|
|
"Please migrate your implementation of `custom_sell` to `custom_exit`.")
|
2022-03-12 08:31:14 +00:00
|
|
|
else:
|
2022-03-12 10:15:27 +00:00
|
|
|
# TODO: Implementing one of the following methods should show a deprecation warning
|
|
|
|
# buy_trend and sell_trend, custom_sell
|
2022-03-12 09:57:03 +00:00
|
|
|
if (
|
2022-03-12 10:15:27 +00:00
|
|
|
not check_override(strategy, IStrategy, 'populate_buy_trend')
|
|
|
|
and not check_override(strategy, IStrategy, 'populate_entry_trend')
|
2022-03-12 09:57:03 +00:00
|
|
|
):
|
2022-03-12 08:49:20 +00:00
|
|
|
raise OperationalException(
|
|
|
|
"`populate_entry_trend` or `populate_buy_trend` must be implemented.")
|
2022-03-12 09:57:03 +00:00
|
|
|
if (
|
2022-03-12 10:15:27 +00:00
|
|
|
not check_override(strategy, IStrategy, 'populate_sell_trend')
|
|
|
|
and not check_override(strategy, IStrategy, 'populate_exit_trend')
|
2022-03-12 09:57:03 +00:00
|
|
|
):
|
2022-03-12 08:49:20 +00:00
|
|
|
raise OperationalException(
|
|
|
|
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
|
|
|
|
|
2022-03-12 08:31:14 +00:00
|
|
|
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
|
|
|
|
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
|
|
|
|
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
|
|
|
|
if any(x == 2 for x in [
|
|
|
|
strategy._populate_fun_len,
|
|
|
|
strategy._buy_fun_len,
|
|
|
|
strategy._sell_fun_len
|
|
|
|
]):
|
|
|
|
strategy.INTERFACE_VERSION = 1
|
2019-07-21 12:52:59 +00:00
|
|
|
|
2019-09-07 23:43:02 +00:00
|
|
|
return strategy
|
2019-07-21 12:52:59 +00:00
|
|
|
|
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-03-12 09:57:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
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 09:57:03 +00:00
|
|
|
"""
|
2022-03-12 10:15:27 +00:00
|
|
|
return getattr(type(object), attribute) != getattr(parentclass, attribute)
|