Merge branch 'develop' into feat/objectify-ccxt
This commit is contained in:
@@ -7,8 +7,6 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.indicator_helpers import fishers_inverse
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
class_name = 'DefaultStrategy'
|
||||
|
||||
|
||||
class DefaultStrategy(IStrategy):
|
||||
"""
|
||||
|
@@ -33,7 +33,6 @@ class IStrategy(ABC):
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@@ -41,5 +40,5 @@ class IStrategy(ABC):
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
:return: DataFrame with sell column
|
||||
"""
|
||||
|
130
freqtrade/strategy/resolver.py
Normal file
130
freqtrade/strategy/resolver.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# pragma pylint: disable=attribute-defined-outside-init
|
||||
|
||||
"""
|
||||
This module load custom strategies
|
||||
"""
|
||||
import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Dict, Type
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StrategyResolver(object):
|
||||
"""
|
||||
This class contains all the logic to load custom strategy class
|
||||
"""
|
||||
|
||||
__slots__ = ['strategy']
|
||||
|
||||
def __init__(self, config: Optional[Dict] = None) -> None:
|
||||
"""
|
||||
Load the custom class from config parameter
|
||||
:param config: configuration dictionary or None
|
||||
"""
|
||||
config = config or {}
|
||||
|
||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
||||
self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path'))
|
||||
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
if 'minimal_roi' in config:
|
||||
self.strategy.minimal_roi = config['minimal_roi']
|
||||
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
||||
|
||||
if 'stoploss' in config:
|
||||
self.strategy.stoploss = config['stoploss']
|
||||
logger.info(
|
||||
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
||||
)
|
||||
|
||||
if 'ticker_interval' in config:
|
||||
self.strategy.ticker_interval = config['ticker_interval']
|
||||
logger.info(
|
||||
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
||||
config['ticker_interval']
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
def _load_strategy(
|
||||
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:
|
||||
"""
|
||||
Search and loads the specified strategy.
|
||||
:param strategy_name: name of the module to import
|
||||
:param extra_dir: additional directory to search for the given strategy
|
||||
:return: Strategy instance or None
|
||||
"""
|
||||
current_path = os.path.dirname(os.path.realpath(__file__))
|
||||
abs_paths = [
|
||||
os.path.join(current_path, '..', '..', 'user_data', 'strategies'),
|
||||
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:
|
||||
strategy = self._search_strategy(path, strategy_name)
|
||||
if strategy:
|
||||
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
||||
return strategy
|
||||
|
||||
raise ImportError(
|
||||
"Impossible to load Strategy '{}'. This class does not exist"
|
||||
" or contains Python code errors".format(strategy_name)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_valid_strategies(module_path: str, strategy_name: str) -> Optional[Type[IStrategy]]:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _search_strategy(directory: str, strategy_name: str) -> Optional[IStrategy]:
|
||||
"""
|
||||
Search for the strategy_name in the given directory
|
||||
:param directory: relative or absolute directory path
|
||||
:return: name of the strategy class
|
||||
"""
|
||||
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'):
|
||||
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
|
@@ -1,169 +0,0 @@
|
||||
# pragma pylint: disable=attribute-defined-outside-init
|
||||
|
||||
"""
|
||||
This module load custom strategies
|
||||
"""
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Constants
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
sys.path.insert(0, r'../../user_data/strategies')
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Strategy(object):
|
||||
"""
|
||||
This class contains all the logic to load custom strategy class
|
||||
"""
|
||||
def __init__(self, config: dict = {}) -> None:
|
||||
"""
|
||||
Load the custom class from config parameter
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||
if 'strategy' in config:
|
||||
strategy = config['strategy']
|
||||
else:
|
||||
strategy = Constants.DEFAULT_STRATEGY
|
||||
|
||||
# Load the strategy
|
||||
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']
|
||||
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
||||
|
||||
if 'stoploss' in config:
|
||||
self.custom_strategy.stoploss = config['stoploss']
|
||||
logger.info(
|
||||
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
||||
)
|
||||
|
||||
if 'ticker_interval' in config:
|
||||
self.custom_strategy.ticker_interval = config['ticker_interval']
|
||||
logger.info(
|
||||
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
||||
config['ticker_interval']
|
||||
)
|
||||
|
||||
# Minimal ROI designed for the strategy
|
||||
self.minimal_roi = OrderedDict(sorted(
|
||||
{int(key): value for (key, value) in self.custom_strategy.minimal_roi.items()}.items(),
|
||||
key=lambda t: t[0])) # sort after converting to number
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
self.stoploss = float(self.custom_strategy.stoploss)
|
||||
|
||||
self.ticker_interval = self.custom_strategy.ticker_interval
|
||||
|
||||
def _load_strategy(self, strategy_name: str) -> None:
|
||||
"""
|
||||
Search and load the custom strategy. If no strategy found, fallback on the default strategy
|
||||
Set the object into self.custom_strategy
|
||||
:param strategy_name: name of the module to import
|
||||
:return: None
|
||||
"""
|
||||
|
||||
try:
|
||||
# Start by sanitizing the file name (remove any extensions)
|
||||
strategy_name = self._sanitize_module_name(filename=strategy_name)
|
||||
|
||||
# Search where can be the strategy file
|
||||
path = self._search_strategy(filename=strategy_name)
|
||||
|
||||
# Load the strategy
|
||||
self.custom_strategy = self._load_class(path + strategy_name)
|
||||
|
||||
# Fallback to the default strategy
|
||||
except (ImportError, TypeError) as error:
|
||||
logger.error(
|
||||
"Impossible to load Strategy 'user_data/strategies/%s.py'. This file does not exist"
|
||||
" or contains Python code errors",
|
||||
strategy_name
|
||||
)
|
||||
logger.error(
|
||||
"The error is:\n%s.",
|
||||
error
|
||||
)
|
||||
|
||||
def _load_class(self, filename: str) -> IStrategy:
|
||||
"""
|
||||
Import a strategy as a module
|
||||
:param filename: path to the strategy (path from freqtrade/strategy/)
|
||||
:return: return the strategy class
|
||||
"""
|
||||
module = importlib.import_module(filename, __package__)
|
||||
custom_strategy = getattr(module, module.class_name)
|
||||
|
||||
logger.info("Load strategy class: %s (%s.py)", module.class_name, filename)
|
||||
return custom_strategy()
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_module_name(filename: str) -> str:
|
||||
"""
|
||||
Remove any extension from filename
|
||||
:param filename: filename to sanatize
|
||||
:return: return the filename without extensions
|
||||
"""
|
||||
filename = os.path.basename(filename)
|
||||
filename = os.path.splitext(filename)[0]
|
||||
return filename
|
||||
|
||||
@staticmethod
|
||||
def _search_strategy(filename: str) -> str:
|
||||
"""
|
||||
Search for the Strategy file in different folder
|
||||
1. search into the user_data/strategies folder
|
||||
2. search into the freqtrade/strategy folder
|
||||
3. if nothing found, return None
|
||||
:param strategy_name: module name to search
|
||||
:return: module path where is the strategy
|
||||
"""
|
||||
pwd = os.path.dirname(os.path.realpath(__file__)) + '/'
|
||||
user_data = os.path.join(pwd, '..', '..', 'user_data', 'strategies', filename + '.py')
|
||||
strategy_folder = os.path.join(pwd, filename + '.py')
|
||||
|
||||
path = None
|
||||
if os.path.isfile(user_data):
|
||||
path = 'user_data.strategies.'
|
||||
elif os.path.isfile(strategy_folder):
|
||||
path = '.'
|
||||
|
||||
return path
|
||||
|
||||
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)
|
Reference in New Issue
Block a user