Merge pull request #588 from gcarq/feature/enhance-strategy-resolving-2

Add --strategy-path parameter and simplify StrategyResolver
This commit is contained in:
Janne Sinivirta 2018-03-28 10:54:24 +03:00 committed by GitHub
commit 131dfaf263
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 168 additions and 150 deletions

View File

@ -48,5 +48,7 @@
"initial_state": "running", "initial_state": "running",
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
} },
"strategy": "DefaultStrategy",
"strategy_path": "/some/folder/"
} }

View File

@ -42,6 +42,13 @@ You can test it with the parameter: `--strategy TestStrategy`
python3 ./freqtrade/main.py --strategy AwesomeStrategy python3 ./freqtrade/main.py --strategy AwesomeStrategy
``` ```
### Specify custom strategy location
If you want to use a strategy from a different folder you can pass `--strategy-path`
```bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
```
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py) **For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
file as reference.** file as reference.**

View File

@ -28,6 +28,7 @@ optional arguments:
specify configuration file (default: config.json) specify configuration file (default: config.json)
-s NAME, --strategy NAME -s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy) specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path
--dry-run-db Force dry run to use a local DB --dry-run-db Force dry run to use a local DB
"tradesv3.dry_run.sqlite" instead of memory DB. Work "tradesv3.dry_run.sqlite" instead of memory DB. Work
only if dry_run is enabled. only if dry_run is enabled.
@ -67,9 +68,16 @@ message the reason (File not found, or errors in your code).
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md). Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
### How to use --strategy-path?
This parameter allows you to add an additional strategy lookup path, which gets
checked before the default locations (The passed path must be a folder!):
```bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
```
#### How to install a strategy? #### How to install a strategy?
This is very simple. Copy paste your strategy file into the folder This is very simple. Copy paste your strategy file into the folder
`user_data/strategies`. And voila, the bot is ready to use it. `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
### How to use --dynamic-whitelist? ### How to use --dynamic-whitelist?
Per default `--dynamic-whitelist` will retrieve the 20 currencies based Per default `--dynamic-whitelist` will retrieve the 20 currencies based

View File

@ -35,6 +35,8 @@ The table below will list all configuration parameters.
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
| `initial_state` | running | No | Defines the initial application state. More information below. | `initial_state` | running | No | Defines the initial application state. More information below.
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second. | `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
The definition of each config parameters is in The definition of each config parameters is in

View File

@ -36,7 +36,7 @@ class Analyze(object):
:param config: Bot configuration (use the one from Configuration()) :param config: Bot configuration (use the one from Configuration())
""" """
self.config = config self.config = config
self.strategy = StrategyResolver(self.config) self.strategy = StrategyResolver(self.config).strategy
@staticmethod @staticmethod
def parse_ticker_dataframe(ticker: list) -> DataFrame: def parse_ticker_dataframe(ticker: list) -> DataFrame:

View File

@ -86,6 +86,13 @@ class Arguments(object):
type=str, type=str,
metavar='NAME', metavar='NAME',
) )
self.parser.add_argument(
'--strategy-path',
help='specify additional strategy lookup path',
dest='strategy_path',
type=str,
metavar='PATH',
)
self.parser.add_argument( self.parser.add_argument(
'--dynamic-whitelist', '--dynamic-whitelist',
help='dynamically generate and update whitelist \ help='dynamically generate and update whitelist \

View File

@ -33,9 +33,13 @@ class Configuration(object):
logger.info('Using config: %s ...', self.args.config) logger.info('Using config: %s ...', self.args.config)
config = self._load_config_file(self.args.config) config = self._load_config_file(self.args.config)
# Add the strategy file to use # Set strategy if not specified in config and or if it's non default
if self.args.strategy != Constants.DEFAULT_STRATEGY or not config.get('strategy'):
config.update({'strategy': self.args.strategy}) config.update({'strategy': self.args.strategy})
if self.args.strategy_path:
config.update({'strategy_path': self.args.strategy_path})
# Load Common configuration # Load Common configuration
config = self._load_common_config(config) config = self._load_common_config(config)

View File

@ -3,7 +3,6 @@
Main Freqtrade bot script. Main Freqtrade bot script.
Read the documentation to know what cli arguments you need. Read the documentation to know what cli arguments you need.
""" """
import logging import logging
import sys import sys
from typing import List from typing import List

View File

@ -33,7 +33,6 @@ class IStrategy(ABC):
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:return: DataFrame with buy column :return: DataFrame with buy column
:return:
""" """
@abstractmethod @abstractmethod
@ -41,5 +40,5 @@ class IStrategy(ABC):
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:return: DataFrame with buy column :return: DataFrame with sell column
""" """

View File

@ -10,8 +10,6 @@ import os
from collections import OrderedDict from collections import OrderedDict
from typing import Optional, Dict, Type from typing import Optional, Dict, Type
from pandas import DataFrame
from freqtrade.constants import Constants from freqtrade.constants import Constants
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
@ -23,81 +21,73 @@ class StrategyResolver(object):
""" """
This class contains all the logic to load custom strategy class This class contains all the logic to load custom strategy class
""" """
__slots__ = ['strategy']
def __init__(self, config: Optional[Dict] = None) -> None: def __init__(self, config: Optional[Dict] = None) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: :param config: configuration dictionary or None
:return:
""" """
config = config or {} config = config or {}
# Verify the strategy is in the configuration, otherwise fallback to the default strategy # Verify the strategy is in the configuration, otherwise fallback to the default strategy
if 'strategy' in config: strategy_name = config.get('strategy') or Constants.DEFAULT_STRATEGY
strategy = config['strategy'] self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path'))
else:
strategy = Constants.DEFAULT_STRATEGY
# Try to load the strategy
self._load_strategy(strategy)
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration
if 'minimal_roi' in config: if 'minimal_roi' in config:
self.custom_strategy.minimal_roi = config['minimal_roi'] self.strategy.minimal_roi = config['minimal_roi']
logger.info("Override strategy \'minimal_roi\' with value in config file.") logger.info("Override strategy \'minimal_roi\' with value in config file.")
if 'stoploss' in config: if 'stoploss' in config:
self.custom_strategy.stoploss = config['stoploss'] self.strategy.stoploss = config['stoploss']
logger.info( logger.info(
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss'] "Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
) )
if 'ticker_interval' in config: if 'ticker_interval' in config:
self.custom_strategy.ticker_interval = config['ticker_interval'] self.strategy.ticker_interval = config['ticker_interval']
logger.info( logger.info(
"Override strategy \'ticker_interval\' with value in config file: %s.", "Override strategy \'ticker_interval\' with value in config file: %s.",
config['ticker_interval'] config['ticker_interval']
) )
# Minimal ROI designed for the strategy # Sort and apply type conversions
self.minimal_roi = OrderedDict(sorted( self.strategy.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in self.custom_strategy.minimal_roi.items()}.items(), {int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
key=lambda t: t[0])) # sort after converting to number key=lambda t: t[0]))
self.strategy.stoploss = float(self.strategy.stoploss)
self.strategy.ticker_interval = int(self.strategy.ticker_interval)
# Optimal stoploss designed for the strategy def _load_strategy(
self.stoploss = float(self.custom_strategy.stoploss) self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:
self.ticker_interval = int(self.custom_strategy.ticker_interval)
def _load_strategy(self, strategy_name: str) -> None:
""" """
Search and loads the specified strategy. Search and loads the specified strategy.
:param strategy_name: name of the module to import :param strategy_name: name of the module to import
:return: None :param extra_dir: additional directory to search for the given strategy
:return: Strategy instance or None
""" """
try:
current_path = os.path.dirname(os.path.realpath(__file__)) current_path = os.path.dirname(os.path.realpath(__file__))
abs_paths = [ abs_paths = [
os.path.join(current_path, '..', '..', 'user_data', 'strategies'), os.path.join(current_path, '..', '..', 'user_data', 'strategies'),
current_path, current_path,
] ]
for path in abs_paths:
self.custom_strategy = self._search_strategy(path, strategy_name)
if self.custom_strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
return None
raise ImportError('not found') if extra_dir:
# Fallback to the default strategy # Add extra strategy directory on top of search paths
except (ImportError, TypeError) as error: abs_paths.insert(0, extra_dir)
logger.error(
"Impossible to load Strategy '%s'. This class does not exist" for path in abs_paths:
" or contains Python code errors", strategy = self._search_strategy(path, strategy_name)
strategy_name if strategy:
) logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
logger.error( return strategy
"The error is:\n%s.",
error raise ImportError(
"Impossible to load Strategy '{}'. This class does not exist"
" or contains Python code errors".format(strategy_name)
) )
@staticmethod @staticmethod
@ -139,28 +129,3 @@ class StrategyResolver(object):
if strategy: if strategy:
return strategy() return strategy()
return None return None
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)

View File

@ -1,9 +1,10 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import logging import logging
import os import os
import pytest
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.resolver import StrategyResolver from freqtrade.strategy.resolver import StrategyResolver
@ -19,46 +20,50 @@ def test_search_strategy():
def test_load_strategy(result): def test_load_strategy(result):
resolver = StrategyResolver()
resolver._load_strategy('TestStrategy')
assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_strategy_custom_directory(result):
resolver = StrategyResolver()
extra_dir = os.path.join('some', 'path')
with pytest.raises(
FileNotFoundError,
match=r".*No such file or directory: '{}'".format(extra_dir)):
resolver._load_strategy('TestStrategy', extra_dir)
assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_not_found_strategy():
strategy = StrategyResolver() strategy = StrategyResolver()
with pytest.raises(ImportError,
assert not hasattr(StrategyResolver, 'custom_strategy') match=r'Impossible to load Strategy \'NotFoundStrategy\'.'
strategy._load_strategy('TestStrategy') r' This class does not exist or contains Python code errors'):
assert not hasattr(StrategyResolver, 'custom_strategy')
assert hasattr(strategy.custom_strategy, 'populate_indicators')
assert 'adx' in strategy.populate_indicators(result)
def test_load_not_found_strategy(caplog):
strategy = StrategyResolver()
assert not hasattr(StrategyResolver, 'custom_strategy')
strategy._load_strategy('NotFoundStrategy') strategy._load_strategy('NotFoundStrategy')
error_msg = "Impossible to load Strategy '{}'. This class does not " \
"exist or contains Python code errors".format('NotFoundStrategy')
assert ('freqtrade.strategy.resolver', logging.ERROR, error_msg) in caplog.record_tuples
def test_strategy(result): def test_strategy(result):
strategy = StrategyResolver({'strategy': 'DefaultStrategy'}) resolver = StrategyResolver({'strategy': 'DefaultStrategy'})
assert hasattr(strategy.custom_strategy, 'minimal_roi') assert hasattr(resolver.strategy, 'minimal_roi')
assert strategy.minimal_roi[0] == 0.04 assert resolver.strategy.minimal_roi[0] == 0.04
assert hasattr(strategy.custom_strategy, 'stoploss') assert hasattr(resolver.strategy, 'stoploss')
assert strategy.stoploss == -0.10 assert resolver.strategy.stoploss == -0.10
assert hasattr(strategy.custom_strategy, 'populate_indicators') assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in strategy.populate_indicators(result) assert 'adx' in resolver.strategy.populate_indicators(result)
assert hasattr(strategy.custom_strategy, 'populate_buy_trend') assert hasattr(resolver.strategy, 'populate_buy_trend')
dataframe = strategy.populate_buy_trend(strategy.populate_indicators(result)) dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result))
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
assert hasattr(strategy.custom_strategy, 'populate_sell_trend') assert hasattr(resolver.strategy, 'populate_sell_trend')
dataframe = strategy.populate_sell_trend(strategy.populate_indicators(result)) dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result))
assert 'sell' in dataframe.columns assert 'sell' in dataframe.columns
@ -70,10 +75,10 @@ def test_strategy_override_minimal_roi(caplog):
"0": 0.5 "0": 0.5
} }
} }
strategy = StrategyResolver(config) resolver = StrategyResolver(config)
assert hasattr(strategy.custom_strategy, 'minimal_roi') assert hasattr(resolver.strategy, 'minimal_roi')
assert strategy.minimal_roi[0] == 0.5 assert resolver.strategy.minimal_roi[0] == 0.5
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'minimal_roi\' with value in config file.' 'Override strategy \'minimal_roi\' with value in config file.'
@ -86,10 +91,10 @@ def test_strategy_override_stoploss(caplog):
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'stoploss': -0.5 'stoploss': -0.5
} }
strategy = StrategyResolver(config) resolver = StrategyResolver(config)
assert hasattr(strategy.custom_strategy, 'stoploss') assert hasattr(resolver.strategy, 'stoploss')
assert strategy.stoploss == -0.5 assert resolver.strategy.stoploss == -0.5
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'stoploss\' with value in config file: -0.5.' 'Override strategy \'stoploss\' with value in config file: -0.5.'
@ -103,31 +108,11 @@ def test_strategy_override_ticker_interval(caplog):
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'ticker_interval': 60 'ticker_interval': 60
} }
strategy = StrategyResolver(config) resolver = StrategyResolver(config)
assert hasattr(strategy.custom_strategy, 'ticker_interval') assert hasattr(resolver.strategy, 'ticker_interval')
assert strategy.ticker_interval == 60 assert resolver.strategy.ticker_interval == 60
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'ticker_interval\' with value in config file: 60.' 'Override strategy \'ticker_interval\' with value in config file: 60.'
) in caplog.record_tuples ) in caplog.record_tuples
def test_strategy_fallback_default_strategy():
strategy = StrategyResolver()
strategy.logger = logging.getLogger(__name__)
assert not hasattr(StrategyResolver, 'custom_strategy')
strategy._load_strategy('../../super_duper')
assert not hasattr(StrategyResolver, 'custom_strategy')
def test_strategy_singleton():
strategy1 = StrategyResolver({'strategy': 'DefaultStrategy'})
assert hasattr(strategy1.custom_strategy, 'minimal_roi')
assert strategy1.minimal_roi[0] == 0.04
strategy2 = StrategyResolver()
assert hasattr(strategy2.custom_strategy, 'minimal_roi')
assert strategy2.minimal_roi[0] == 0.04

View File

@ -71,6 +71,26 @@ def test_parse_args_invalid() -> None:
Arguments(['-c'], '').get_parsed_arg() Arguments(['-c'], '').get_parsed_arg()
def test_parse_args_strategy() -> None:
args = Arguments(['--strategy', 'SomeStrategy'], '').get_parsed_arg()
assert args.strategy == 'SomeStrategy'
def test_parse_args_strategy_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--strategy'], '').get_parsed_arg()
def test_parse_args_strategy_path() -> None:
args = Arguments(['--strategy-path', '/some/path'], '').get_parsed_arg()
assert args.strategy_path == '/some/path'
def test_parse_args_strategy_path_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--strategy-path'], '').get_parsed_arg()
def test_parse_args_dynamic_whitelist() -> None: def test_parse_args_dynamic_whitelist() -> None:
args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg() args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg()
assert args.dynamic_whitelist == 20 assert args.dynamic_whitelist == 20

View File

@ -98,8 +98,8 @@ def test_load_config(default_conf, mocker) -> None:
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert 'strategy' in validated_conf assert validated_conf.get('strategy') == 'DefaultStrategy'
assert validated_conf['strategy'] == 'DefaultStrategy' assert validated_conf.get('strategy_path') is None
assert 'dynamic_whitelist' not in validated_conf assert 'dynamic_whitelist' not in validated_conf
assert 'dry_run_db' not in validated_conf assert 'dry_run_db' not in validated_conf
@ -115,19 +115,39 @@ def test_load_config_with_params(default_conf, mocker) -> None:
args = [ args = [
'--dynamic-whitelist', '10', '--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--dry-run-db' '--strategy-path', '/some/path',
'--dry-run-db',
] ]
args = Arguments(args, '').get_parsed_arg() args = Arguments(args, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert 'dynamic_whitelist' in validated_conf assert validated_conf.get('dynamic_whitelist') == 10
assert validated_conf['dynamic_whitelist'] == 10 assert validated_conf.get('strategy') == 'TestStrategy'
assert 'strategy' in validated_conf assert validated_conf.get('strategy_path') == '/some/path'
assert validated_conf['strategy'] == 'TestStrategy' assert validated_conf.get('dry_run_db') is True
assert 'dry_run_db' in validated_conf
assert validated_conf['dry_run_db'] is True
def test_load_custom_strategy(default_conf, mocker) -> None:
"""
Test Configuration.load_config() without any cli params
"""
custom_conf = deepcopy(default_conf)
custom_conf.update({
'strategy': 'CustomStrategy',
'strategy_path': '/tmp/strategies',
})
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(custom_conf)
))
args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('strategy') == 'CustomStrategy'
assert validated_conf.get('strategy_path') == '/tmp/strategies'
def test_show_info(default_conf, mocker, caplog) -> None: def test_show_info(default_conf, mocker, caplog) -> None: