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
|
2018-02-06 14:31:50 +00:00
|
|
|
from collections import OrderedDict
|
2019-01-21 18:30:59 +00:00
|
|
|
from inspect import getfullargspec
|
2018-07-30 20:00:08 +00:00
|
|
|
from pathlib import Path
|
2018-11-24 19:00:02 +00:00
|
|
|
from typing import Dict, Optional
|
2018-03-17 21:44:47 +00:00
|
|
|
|
2019-07-12 20:45:49 +00:00
|
|
|
from freqtrade import constants, OperationalException
|
2018-11-24 19:00:02 +00:00
|
|
|
from freqtrade.resolvers import IResolver
|
2018-06-23 09:13:49 +00:00
|
|
|
from freqtrade.strategy import import_strategy
|
2018-01-15 08:35:11 +00:00
|
|
|
from freqtrade.strategy.interface import IStrategy
|
|
|
|
|
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
|
|
|
"""
|
|
|
|
This class contains all the logic to load custom strategy class
|
|
|
|
"""
|
2018-03-24 22:32:17 +00:00
|
|
|
|
|
|
|
__slots__ = ['strategy']
|
|
|
|
|
2018-03-24 17:14:05 +00:00
|
|
|
def __init__(self, config: Optional[Dict] = None) -> None:
|
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 {}
|
|
|
|
|
2018-01-15 08:35:11 +00:00
|
|
|
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
2018-04-02 14:42:53 +00:00
|
|
|
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
2018-05-31 20:17:46 +00:00
|
|
|
self.strategy: IStrategy = self._load_strategy(strategy_name,
|
2018-07-16 05:11:17 +00:00
|
|
|
config=config,
|
2018-05-31 20:17:46 +00:00
|
|
|
extra_dir=config.get('strategy_path'))
|
2019-01-13 18:28:20 +00:00
|
|
|
|
|
|
|
# make sure experimental dict is available
|
|
|
|
if 'experimental' not in config:
|
|
|
|
config['experimental'] = {}
|
|
|
|
|
2018-01-15 08:35:11 +00:00
|
|
|
# Set attributes
|
|
|
|
# Check if we need to override configuration
|
2019-01-25 18:14:38 +00:00
|
|
|
# (Attribute name, default, experimental)
|
2019-03-14 06:56:21 +00:00
|
|
|
attributes = [("minimal_roi", {"0": 10.0}, False),
|
|
|
|
("ticker_interval", None, False),
|
|
|
|
("stoploss", None, False),
|
|
|
|
("trailing_stop", None, False),
|
|
|
|
("trailing_stop_positive", None, False),
|
|
|
|
("trailing_stop_positive_offset", 0.0, False),
|
|
|
|
("trailing_only_offset_is_reached", None, False),
|
|
|
|
("process_only_new_candles", None, False),
|
|
|
|
("order_types", None, False),
|
|
|
|
("order_time_in_force", None, False),
|
2019-03-23 19:40:07 +00:00
|
|
|
("stake_currency", None, False),
|
|
|
|
("stake_amount", None, False),
|
2019-03-14 06:56:21 +00:00
|
|
|
("use_sell_signal", False, True),
|
|
|
|
("sell_profit_only", False, True),
|
|
|
|
("ignore_roi_if_buy_signal", False, True),
|
2019-01-05 06:22:19 +00:00
|
|
|
]
|
2019-01-13 18:28:20 +00:00
|
|
|
for attribute, default, experimental in attributes:
|
|
|
|
if experimental:
|
|
|
|
self._override_attribute_helper(config['experimental'], attribute, default)
|
|
|
|
else:
|
|
|
|
self._override_attribute_helper(config, attribute, default)
|
2019-01-05 06:25:35 +00:00
|
|
|
|
|
|
|
# Loop this list again to have output combined
|
2019-01-13 18:28:20 +00:00
|
|
|
for attribute, _, exp in attributes:
|
|
|
|
if exp and attribute in config['experimental']:
|
|
|
|
logger.info("Strategy using %s: %s", attribute, config['experimental'][attribute])
|
|
|
|
elif attribute in config:
|
2019-01-05 06:24:15 +00:00
|
|
|
logger.info("Strategy using %s: %s", attribute, config[attribute])
|
|
|
|
|
2019-01-05 06:20:38 +00:00
|
|
|
# Sort and apply type conversions
|
|
|
|
self.strategy.minimal_roi = OrderedDict(sorted(
|
|
|
|
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
|
|
|
key=lambda t: t[0]))
|
|
|
|
self.strategy.stoploss = float(self.strategy.stoploss)
|
2018-08-09 17:24:00 +00:00
|
|
|
|
2019-01-05 06:20:38 +00:00
|
|
|
self._strategy_sanity_validations()
|
2018-11-17 09:14:18 +00:00
|
|
|
|
2019-01-13 18:28:20 +00:00
|
|
|
def _override_attribute_helper(self, config, attribute: str, default):
|
|
|
|
"""
|
|
|
|
Override attributes in the strategy.
|
|
|
|
Prevalence:
|
|
|
|
- Configuration
|
|
|
|
- Strategy
|
|
|
|
- default (if not None)
|
|
|
|
"""
|
2019-01-05 06:20:38 +00:00
|
|
|
if attribute in config:
|
|
|
|
setattr(self.strategy, attribute, config[attribute])
|
|
|
|
logger.info("Override strategy '%s' with value in config file: %s.",
|
|
|
|
attribute, config[attribute])
|
|
|
|
elif hasattr(self.strategy, attribute):
|
|
|
|
config[attribute] = getattr(self.strategy, attribute)
|
2019-01-13 18:28:20 +00:00
|
|
|
# Explicitly check for None here as other "falsy" values are possible
|
|
|
|
elif default is not None:
|
|
|
|
setattr(self.strategy, attribute, default)
|
|
|
|
config[attribute] = default
|
2018-11-25 21:02:59 +00:00
|
|
|
|
2019-01-05 06:20:38 +00:00
|
|
|
def _strategy_sanity_validations(self):
|
2018-11-17 11:59:16 +00:00
|
|
|
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
|
|
|
|
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
|
|
|
f"Order-types mapping is incomplete.")
|
|
|
|
|
2018-11-25 21:02:59 +00:00
|
|
|
if not all(k in self.strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF):
|
|
|
|
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
|
|
|
f"Order-time-in-force mapping is incomplete.")
|
|
|
|
|
2018-03-25 14:28:04 +00:00
|
|
|
def _load_strategy(
|
2018-07-16 05:11:17 +00:00
|
|
|
self, 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:39:16 +00:00
|
|
|
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
2018-11-24 19:00:02 +00:00
|
|
|
|
2018-03-25 14:28:04 +00:00
|
|
|
abs_paths = [
|
2018-11-24 19:39:16 +00:00
|
|
|
Path.cwd().joinpath('user_data/strategies'),
|
2018-03-25 14:28:04 +00:00
|
|
|
current_path,
|
|
|
|
]
|
|
|
|
|
|
|
|
if extra_dir:
|
|
|
|
# Add extra strategy directory on top of search paths
|
2018-11-24 19:39:16 +00:00
|
|
|
abs_paths.insert(0, Path(extra_dir).resolve())
|
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
|
|
|
|
2018-11-24 19:02:29 +00:00
|
|
|
for _path in abs_paths:
|
2018-06-23 12:42:22 +00:00
|
|
|
try:
|
2019-07-12 20:45:49 +00:00
|
|
|
(strategy, module_path) = self._search_object(directory=_path,
|
|
|
|
object_type=IStrategy,
|
|
|
|
object_name=strategy_name,
|
|
|
|
kwargs={'config': config})
|
2018-06-23 12:42:22 +00:00
|
|
|
if strategy:
|
2019-07-12 20:45:49 +00:00
|
|
|
logger.info(f"Using resolved strategy {strategy_name} from '{module_path}'...")
|
2018-07-23 16:38:21 +00:00
|
|
|
strategy._populate_fun_len = len(
|
2019-01-21 18:30:59 +00:00
|
|
|
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)
|
2019-04-16 18:25:48 +00:00
|
|
|
try:
|
|
|
|
return import_strategy(strategy, config=config)
|
|
|
|
except TypeError as e:
|
|
|
|
logger.warning(
|
2019-07-12 20:45:49 +00:00
|
|
|
f"Impossible to load strategy '{strategy_name}' from {module_path}. "
|
|
|
|
f"Error: {e}")
|
2018-06-23 12:42:22 +00:00
|
|
|
except FileNotFoundError:
|
2019-07-12 20:45:49 +00:00
|
|
|
logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd()))
|
2018-03-25 14:28:04 +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
|
|
|
)
|