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-24 19:44:04 +00:00
|
|
|
import importlib.util
|
|
|
|
import inspect
|
2018-01-15 08:35:11 +00:00
|
|
|
import os
|
2018-02-06 14:31:50 +00:00
|
|
|
from collections import OrderedDict
|
2018-03-24 19:44:04 +00:00
|
|
|
from typing import Optional, Dict, Type
|
2018-03-17 21:44:47 +00:00
|
|
|
|
2018-01-15 08:35:11 +00:00
|
|
|
from pandas import DataFrame
|
2018-03-17 21:44:47 +00:00
|
|
|
|
2018-02-07 04:22:17 +00:00
|
|
|
from freqtrade.constants import Constants
|
2018-03-17 21:44:47 +00:00
|
|
|
from freqtrade.logger import Logger
|
2018-01-15 08:35:11 +00:00
|
|
|
from freqtrade.strategy.interface import IStrategy
|
|
|
|
|
|
|
|
|
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
|
|
|
|
"""
|
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
|
|
|
|
:param config:
|
|
|
|
:return:
|
|
|
|
"""
|
2018-03-24 17:14:05 +00:00
|
|
|
config = config or {}
|
|
|
|
|
2018-02-07 04:22:17 +00:00
|
|
|
self.logger = Logger(name=__name__).get_logger()
|
2018-01-15 08:35:11 +00:00
|
|
|
|
|
|
|
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
|
|
|
if 'strategy' in config:
|
|
|
|
strategy = config['strategy']
|
|
|
|
else:
|
2018-02-07 04:22:17 +00:00
|
|
|
strategy = Constants.DEFAULT_STRATEGY
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2018-03-24 19:44:04 +00:00
|
|
|
# Try to load the strategy
|
2018-01-15 08:35:11 +00:00
|
|
|
self._load_strategy(strategy)
|
|
|
|
|
|
|
|
# Set attributes
|
|
|
|
# Check if we need to override configuration
|
|
|
|
if 'minimal_roi' in config:
|
|
|
|
self.custom_strategy.minimal_roi = config['minimal_roi']
|
|
|
|
self.logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
|
|
|
|
|
|
|
if 'stoploss' in config:
|
|
|
|
self.custom_strategy.stoploss = config['stoploss']
|
2018-01-20 22:40:41 +00:00
|
|
|
self.logger.info(
|
2018-01-28 05:26:57 +00:00
|
|
|
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
2018-01-20 22:40:41 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
if 'ticker_interval' in config:
|
|
|
|
self.custom_strategy.ticker_interval = config['ticker_interval']
|
|
|
|
self.logger.info(
|
2018-01-28 05:26:57 +00:00
|
|
|
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
|
|
|
config['ticker_interval']
|
2018-01-20 22:40:41 +00:00
|
|
|
)
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2018-01-28 05:26:57 +00:00
|
|
|
# Minimal ROI designed for the strategy
|
2018-02-06 14:31:50 +00:00
|
|
|
self.minimal_roi = OrderedDict(sorted(
|
2018-02-11 13:02:42 +00:00
|
|
|
{int(key): value for (key, value) in self.custom_strategy.minimal_roi.items()}.items(),
|
2018-03-17 23:01:22 +00:00
|
|
|
key=lambda t: t[0])) # sort after converting to number
|
2018-01-28 05:26:57 +00:00
|
|
|
|
|
|
|
# Optimal stoploss designed for the strategy
|
2018-02-11 13:02:42 +00:00
|
|
|
self.stoploss = float(self.custom_strategy.stoploss)
|
2018-01-28 05:26:57 +00:00
|
|
|
|
2018-03-15 22:48:22 +00:00
|
|
|
self.ticker_interval = int(self.custom_strategy.ticker_interval)
|
2018-01-15 08:35:11 +00:00
|
|
|
|
|
|
|
def _load_strategy(self, strategy_name: str) -> None:
|
|
|
|
"""
|
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
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
try:
|
2018-03-24 19:44:04 +00:00
|
|
|
current_path = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
abs_paths = [
|
|
|
|
os.path.join(current_path, '..', '..', 'user_data', 'strategies'),
|
|
|
|
current_path,
|
|
|
|
]
|
|
|
|
for path in abs_paths:
|
|
|
|
self.custom_strategy = self._search_strategy(path, strategy_name)
|
|
|
|
if self.custom_strategy:
|
|
|
|
self.logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
|
|
|
return None
|
|
|
|
|
|
|
|
raise ImportError('not found')
|
2018-01-15 08:35:11 +00:00
|
|
|
# Fallback to the default strategy
|
2018-02-02 09:01:09 +00:00
|
|
|
except (ImportError, TypeError) as error:
|
|
|
|
self.logger.error(
|
2018-03-24 19:44:04 +00:00
|
|
|
"Impossible to load Strategy '%s'. This class does not exist"
|
2018-02-02 09:01:09 +00:00
|
|
|
" or contains Python code errors",
|
|
|
|
strategy_name
|
|
|
|
)
|
|
|
|
self.logger.error(
|
|
|
|
"The error is:\n%s.",
|
|
|
|
error
|
|
|
|
)
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2018-03-24 19:44:04 +00:00
|
|
|
@staticmethod
|
|
|
|
def _get_valid_strategies(module_path: str, strategy_name: str) -> Optional[Type[IStrategy]]:
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
2018-03-24 19:44:04 +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
|
|
|
"""
|
|
|
|
|
2018-03-24 19:44:04 +00:00
|
|
|
# Generate spec based on absolute path
|
|
|
|
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
|
|
|
|
module = importlib.util.module_from_spec(spec)
|
|
|
|
spec.loader.exec_module(module)
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2018-03-24 19:44:04 +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 19:44:04 +00:00
|
|
|
def _search_strategy(self, directory: str, strategy_name: str) -> Optional[IStrategy]:
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
2018-03-24 19:44:04 +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 19:44:04 +00:00
|
|
|
self.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'):
|
|
|
|
self.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()
|
|
|
|
return None
|
2018-01-15 08:35:11 +00:00
|
|
|
|
|
|
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
|
|
|
"""
|
|
|
|
Populate indicators that will be used in the Buy and Sell strategy
|
|
|
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
|
|
|
:return: a Dataframe with all mandatory indicators for the strategies
|
|
|
|
"""
|
|
|
|
return self.custom_strategy.populate_indicators(dataframe)
|
|
|
|
|
|
|
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|
|
|
"""
|
|
|
|
Based on TA indicators, populates the buy signal for the given dataframe
|
|
|
|
:param dataframe: DataFrame
|
|
|
|
:return: DataFrame with buy column
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
return self.custom_strategy.populate_buy_trend(dataframe)
|
|
|
|
|
|
|
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
|
|
|
"""
|
|
|
|
Based on TA indicators, populates the sell signal for the given dataframe
|
|
|
|
:param dataframe: DataFrame
|
|
|
|
:return: DataFrame with buy column
|
|
|
|
"""
|
|
|
|
return self.custom_strategy.populate_sell_trend(dataframe)
|