stable/freqtrade/strategy/resolver.py

143 lines
5.5 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
"""
import importlib.util
import inspect
2018-03-25 19:37:14 +00:00
import logging
2018-01-15 08:35:11 +00:00
import os
2018-02-06 14:31:50 +00:00
from collections import OrderedDict
2018-07-04 07:31:35 +00:00
from typing import Dict, Optional, Type
2018-03-17 21:44:47 +00:00
from freqtrade import constants
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-03-24 17:11:21 +00:00
class StrategyResolver(object):
2018-01-28 05:26:57 +00:00
"""
This class contains all the logic to load custom strategy class
"""
__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
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,
config=config,
2018-05-31 20:17:46 +00:00
extra_dir=config.get('strategy_path'))
2018-01-15 08:35:11 +00:00
# Set attributes
# Check if we need to override configuration
if 'minimal_roi' in config:
self.strategy.minimal_roi = config['minimal_roi']
2018-03-24 20:56:20 +00:00
logger.info("Override strategy \'minimal_roi\' with value in config file.")
else:
config['minimal_roi'] = self.strategy.minimal_roi
2018-01-15 08:35:11 +00:00
if 'stoploss' in config:
self.strategy.stoploss = config['stoploss']
2018-03-24 20:56:20 +00:00
logger.info(
2018-01-28 05:26:57 +00:00
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
)
else:
config['stoploss'] = self.strategy.stoploss
if 'ticker_interval' in config:
self.strategy.ticker_interval = config['ticker_interval']
2018-03-24 20:56:20 +00:00
logger.info(
2018-01-28 05:26:57 +00:00
"Override strategy \'ticker_interval\' with value in config file: %s.",
config['ticker_interval']
)
else:
config['ticker_interval'] = self.strategy.ticker_interval
2018-01-15 08:35:11 +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-01-28 05:26:57 +00:00
2018-03-25 14:28:04 +00:00
def _load_strategy(
self, strategy_name: str, config: dict, 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
"""
2018-03-25 14:28:04 +00:00
current_path = os.path.dirname(os.path.realpath(__file__))
abs_paths = [
os.path.join(os.getcwd(), '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
abs_paths.insert(0, extra_dir)
for path in abs_paths:
try:
strategy = self._search_strategy(path, strategy_name=strategy_name, config=config)
if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
return import_strategy(strategy, config=config)
except FileNotFoundError:
logger.warning('Path "%s" does not exist', path)
2018-03-25 14:28:04 +00:00
raise ImportError(
"Impossible to load Strategy '{}'. This class does not exist"
" or contains Python code errors".format(strategy_name)
)
2018-01-15 08:35:11 +00:00
@staticmethod
def _get_valid_strategies(module_path: str, strategy_name: str) -> Optional[Type[IStrategy]]:
2018-01-15 08:35:11 +00:00
"""
Returns a list of all possible strategies for the given module_path
:param module_path: absolute path to the module
:param strategy_name: Class name of the strategy
:return: Tuple with (name, class) or None
2018-01-15 08:35:11 +00:00
"""
# Generate spec based on absolute path
spec = importlib.util.spec_from_file_location('unknown', module_path)
module = importlib.util.module_from_spec(spec)
2018-06-02 12:18:57 +00:00
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
2018-01-15 08:35:11 +00:00
valid_strategies_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass)
if strategy_name == name and IStrategy in obj.__bases__
)
return next(valid_strategies_gen, None)
2018-01-15 08:35:11 +00:00
2018-03-24 20:56:20 +00:00
@staticmethod
def _search_strategy(directory: str, strategy_name: str, config: dict) -> Optional[IStrategy]:
2018-01-15 08:35:11 +00:00
"""
Search for the strategy_name in the given directory
:param directory: relative or absolute directory path
:return: name of the strategy class
2018-01-15 08:35:11 +00:00
"""
2018-03-24 20:56:20 +00:00
logger.debug('Searching for strategy %s in \'%s\'', strategy_name, directory)
for entry in os.listdir(directory):
# Only consider python files
if not entry.endswith('.py'):
2018-03-24 20:56:20 +00:00
logger.debug('Ignoring %s', entry)
continue
strategy = StrategyResolver._get_valid_strategies(
os.path.abspath(os.path.join(directory, entry)), strategy_name
)
if strategy:
return strategy(config)
return None