diff --git a/Dockerfile b/Dockerfile index 918552526..afafd93c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6.4-slim-stretch +FROM python:3.6.5-slim-stretch # Install TA-lib RUN apt-get update && apt-get -y install curl build-essential && apt-get clean diff --git a/config_full.json.example b/config_full.json.example index 5362ee943..77ef0faa0 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -48,5 +48,7 @@ "initial_state": "running", "internals": { "process_throttle_secs": 5 - } + }, + "strategy": "DefaultStrategy", + "strategy_path": "/some/folder/" } diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 6911e9e20..b9ff3fe40 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -14,12 +14,12 @@ Since the version `0.16.0` the bot allows using custom strategy file. This is very simple. Copy paste your strategy file into the folder `user_data/strategies`. -Let assume you have a strategy file `awesome-strategy.py`: +Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: 1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py` -2. Start the bot with the param `--strategy awesome-strategy` (the parameter is the name of the file without '.py') +2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) ```bash -python3 ./freqtrade/main.py --strategy awesome_strategy +python3 ./freqtrade/main.py --strategy AwesomeStrategy ``` ## Change your strategy @@ -35,11 +35,18 @@ A strategy file contains all the information needed to build a good strategy: - Stoploss recommended - Hyperopt parameter -The bot also include a sample strategy you can update: `user_data/strategies/test_strategy.py`. -You can test it with the parameter: `--strategy test_strategy` +The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`. +You can test it with the parameter: `--strategy TestStrategy` ```bash -python3 ./freqtrade/main.py --strategy awesome_strategy +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) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 76e693592..95f37e492 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -26,9 +26,9 @@ optional arguments: --version show program's version number and exit -c PATH, --config PATH specify configuration file (default: config.json) - -s PATH, --strategy PATH - specify strategy file (default: - freqtrade/strategy/default_strategy.py) + -s NAME, --strategy NAME + 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 "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is enabled. @@ -48,21 +48,19 @@ python3 ./freqtrade/main.py -c path/far/far/away/config.json ``` ### How to use --strategy? -This parameter will allow you to load your custom strategy file. Per -default without `--strategy` or `-s` the bot will load the -`default_strategy` included with the bot (`freqtrade/strategy/default_strategy.py`). +This parameter will allow you to load your custom strategy class. +Per default without `--strategy` or `-s` the bot will load the +`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`). -The bot will search your strategy file into `user_data/strategies` and -`freqtrade/strategy`. +The bot will search your strategy file within `user_data/strategies` and `freqtrade/strategy`. -To load a strategy, simply pass the file name (without .py) in this -parameters. +To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this parameter. **Example:** -In `user_data/strategies` you have a file `my_awesome_strategy.py` to -load it: +In `user_data/strategies` you have a file `my_awesome_strategy.py` which has +a strategy class called `AwesomeStrategy` to load it: ```bash -python3 ./freqtrade/main.py --strategy my_awesome_strategy +python3 ./freqtrade/main.py --strategy AwesomeStrategy ``` If the bot does not find your strategy file, it will display in an error @@ -70,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). +### 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? 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? Per default `--dynamic-whitelist` will retrieve the 20 currencies based diff --git a/docs/configuration.md b/docs/configuration.md index 2beab348d..3d36947c3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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.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. +| `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. The definition of each config parameters is in diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 9e83f004e..e041fa2ff 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -127,3 +127,14 @@ Day Profit BTC Profit USD ## /version > **Version:** `0.14.3` + +### using proxy with telegram +in [freqtrade/freqtrade/rpc/telegram.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/rpc/telegram.py) replace +``` +self._updater = Updater(token=self._config['telegram']['token'], workers=0) +``` + +with +``` +self._updater = Updater(token=self._config['telegram']['token'], request_kwargs={'proxy_url': 'socks5://127.0.0.1:1080/'}, workers=0) +``` diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index b843db3f4..dcb5376ce 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -9,10 +9,10 @@ from typing import Dict, List, Tuple import arrow from pandas import DataFrame, to_datetime +from freqtrade import constants from freqtrade.exchange import get_ticker_history from freqtrade.persistence import Trade -from freqtrade.strategy.strategy import Strategy -from freqtrade.constants import Constants +from freqtrade.strategy.resolver import StrategyResolver logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ class Analyze(object): :param config: Bot configuration (use the one from Configuration()) """ self.config = config - self.strategy = Strategy(self.config) + self.strategy = StrategyResolver(self.config).strategy @staticmethod def parse_ticker_dataframe(ticker: list) -> DataFrame: @@ -54,7 +54,14 @@ class Analyze(object): utc=True, infer_datetime_format=True) - frame.sort_values('date', inplace=True) + # group by index and aggregate results to eliminate duplicate ticks + frame = frame.groupby(by='date', as_index=False, sort=True).agg({ + 'open': 'first', + 'high': 'max', + 'low': 'min', + 'close': 'last', + 'volume': 'max', + }) return frame def populate_indicators(self, dataframe: DataFrame) -> DataFrame: @@ -139,7 +146,7 @@ class Analyze(object): # Check if dataframe is out of date signal_date = arrow.get(latest['date']) - interval_minutes = Constants.TICKER_INTERVAL_MINUTES[interval] + interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] if signal_date < arrow.utcnow() - timedelta(minutes=(interval_minutes + 5)): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 574deb304..1036e0666 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -8,8 +8,7 @@ import os import re from typing import List, Tuple, Optional -from freqtrade import __version__ -from freqtrade.constants import Constants +from freqtrade import __version__, constants class Arguments(object): @@ -80,9 +79,16 @@ class Arguments(object): ) self.parser.add_argument( '-s', '--strategy', - help='specify strategy file (default: %(default)s)', + help='specify strategy class name (default: %(default)s)', dest='strategy', - default='default_strategy', + default='DefaultStrategy', + type=str, + metavar='NAME', + ) + self.parser.add_argument( + '--strategy-path', + help='specify additional strategy lookup path', + dest='strategy_path', type=str, metavar='PATH', ) @@ -91,7 +97,7 @@ class Arguments(object): help='dynamically generate and update whitelist \ based on 24h BaseVolume (Default 20 currencies)', # noqa dest='dynamic_whitelist', - const=Constants.DYNAMIC_WHITELIST, + const=constants.DYNAMIC_WHITELIST, type=int, metavar='INT', nargs='?', @@ -162,7 +168,7 @@ class Arguments(object): '-e', '--epochs', help='specify number of epochs (default: %(default)d)', dest='epochs', - default=Constants.HYPEROPT_EPOCH, + default=constants.HYPEROPT_EPOCH, type=int, metavar='INT', ) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 32ebabe18..43d0a0bf9 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -10,8 +10,7 @@ from jsonschema import Draft4Validator, validate from jsonschema.exceptions import ValidationError, best_match import ccxt -from freqtrade import OperationalException -from freqtrade.constants import Constants +from freqtrade import OperationalException, constants logger = logging.getLogger(__name__) @@ -34,8 +33,12 @@ class Configuration(object): logger.info('Using config: %s ...', self.args.config) config = self._load_config_file(self.args.config) - # Add the strategy file to use - config.update({'strategy': self.args.strategy}) + # 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}) + + if self.args.strategy_path: + config.update({'strategy_path': self.args.strategy_path}) # Load Common configuration config = self._load_common_config(config) @@ -186,7 +189,7 @@ class Configuration(object): :return: Returns the config if valid, otherwise throw an exception """ try: - validate(conf, Constants.CONF_SCHEMA) + validate(conf, constants.CONF_SCHEMA) return conf except ValidationError as exception: logger.fatal( @@ -194,7 +197,7 @@ class Configuration(object): exception ) raise ValidationError( - best_match(Draft4Validator(Constants.CONF_SCHEMA).iter_errors(conf)).message + best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message ) def get_config(self) -> Dict[str, Any]: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 802785891..fa09d8eab 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -1,136 +1,131 @@ # pragma pylint: disable=too-few-public-methods """ -List bot constants +bot constants """ +DYNAMIC_WHITELIST = 20 # pairs +PROCESS_THROTTLE_SECS = 5 # sec +TICKER_INTERVAL = 5 # min +HYPEROPT_EPOCH = 100 # epochs +RETRY_TIMEOUT = 30 # sec +DEFAULT_STRATEGY = 'DefaultStrategy' + +TICKER_INTERVAL_MINUTES = { + '1m': 1, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': 360, + '12h': 720, + '1d': 1440, + '1w': 10080, +} -class Constants(object): - """ - Static class that contain all bot constants - """ - DYNAMIC_WHITELIST = 20 # pairs - PROCESS_THROTTLE_SECS = 5 # sec - TICKER_INTERVAL = 5 # min - HYPEROPT_EPOCH = 100 # epochs - RETRY_TIMEOUT = 30 # sec - DEFAULT_STRATEGY = 'default_strategy' - - TICKER_INTERVAL_MINUTES = { - '1m': 1, - '5m': 5, - '15m': 15, - '30m': 30, - '1h': 60, - '2h': 120, - '4h': 240, - '6h': 360, - '12h': 720, - '1d': 1440, - '1w': 10080, - } - - # Required json-schema for user specified config - CONF_SCHEMA = { - 'type': 'object', - 'properties': { - 'max_open_trades': {'type': 'integer', 'minimum': 1}, - 'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, - 'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']}, - 'stake_amount': {'type': 'number', 'minimum': 0.0005}, - 'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF', - 'CLP', 'CNY', 'CZK', 'DKK', - 'EUR', 'GBP', 'HKD', 'HUF', - 'IDR', 'ILS', 'INR', 'JPY', - 'KRW', 'MXN', 'MYR', 'NOK', - 'NZD', 'PHP', 'PKR', 'PLN', - 'RUB', 'SEK', 'SGD', 'THB', - 'TRY', 'TWD', 'ZAR', 'USD']}, - 'dry_run': {'type': 'boolean'}, - 'minimal_roi': { - 'type': 'object', - 'patternProperties': { - '^[0-9.]+$': {'type': 'number'} +# Required json-schema for user specified config +CONF_SCHEMA = { + 'type': 'object', + 'properties': { + 'max_open_trades': {'type': 'integer', 'minimum': 1}, + 'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, + 'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']}, + 'stake_amount': {'type': 'number', 'minimum': 0.0005}, + 'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF', + 'CLP', 'CNY', 'CZK', 'DKK', + 'EUR', 'GBP', 'HKD', 'HUF', + 'IDR', 'ILS', 'INR', 'JPY', + 'KRW', 'MXN', 'MYR', 'NOK', + 'NZD', 'PHP', 'PKR', 'PLN', + 'RUB', 'SEK', 'SGD', 'THB', + 'TRY', 'TWD', 'ZAR', 'USD']}, + 'dry_run': {'type': 'boolean'}, + 'minimal_roi': { + 'type': 'object', + 'patternProperties': { + '^[0-9.]+$': {'type': 'number'} + }, + 'minProperties': 1 + }, + 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, + 'unfilledtimeout': {'type': 'integer', 'minimum': 0}, + 'bid_strategy': { + 'type': 'object', + 'properties': { + 'ask_last_balance': { + 'type': 'number', + 'minimum': 0, + 'maximum': 1, + 'exclusiveMaximum': False }, - 'minProperties': 1 }, - 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, - 'unfilledtimeout': {'type': 'integer', 'minimum': 0}, - 'bid_strategy': { - 'type': 'object', - 'properties': { - 'ask_last_balance': { - 'type': 'number', - 'minimum': 0, - 'maximum': 1, - 'exclusiveMaximum': False - }, - }, - 'required': ['ask_last_balance'] - }, - 'exchange': {'$ref': '#/definitions/exchange'}, - 'experimental': { - 'type': 'object', - 'properties': { - 'use_sell_signal': {'type': 'boolean'}, - 'sell_profit_only': {'type': 'boolean'} - } - }, - 'telegram': { - 'type': 'object', - 'properties': { - 'enabled': {'type': 'boolean'}, - 'token': {'type': 'string'}, - 'chat_id': {'type': 'string'}, - }, - 'required': ['enabled', 'token', 'chat_id'] - }, - 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, - 'internals': { - 'type': 'object', - 'properties': { - 'process_throttle_secs': {'type': 'number'}, - 'interval': {'type': 'integer'} - } + 'required': ['ask_last_balance'] + }, + 'exchange': {'$ref': '#/definitions/exchange'}, + 'experimental': { + 'type': 'object', + 'properties': { + 'use_sell_signal': {'type': 'boolean'}, + 'sell_profit_only': {'type': 'boolean'} } }, - 'definitions': { - 'exchange': { - 'type': 'object', - 'properties': { - 'name': {'type': 'string'}, - 'key': {'type': 'string'}, - 'secret': {'type': 'string'}, - 'pair_whitelist': { - 'type': 'array', - 'items': { - 'type': 'string', - 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' - }, - 'uniqueItems': True - }, - 'pair_blacklist': { - 'type': 'array', - 'items': { - 'type': 'string', - 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' - }, - 'uniqueItems': True - } - }, - 'required': ['name', 'key', 'secret', 'pair_whitelist'] - } + 'telegram': { + 'type': 'object', + 'properties': { + 'enabled': {'type': 'boolean'}, + 'token': {'type': 'string'}, + 'chat_id': {'type': 'string'}, + }, + 'required': ['enabled', 'token', 'chat_id'] }, - 'anyOf': [ - {'required': ['exchange']} - ], - 'required': [ - 'max_open_trades', - 'stake_currency', - 'stake_amount', - 'fiat_display_currency', - 'dry_run', - 'bid_strategy', - 'telegram' - ] - } + 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, + 'internals': { + 'type': 'object', + 'properties': { + 'process_throttle_secs': {'type': 'number'}, + 'interval': {'type': 'integer'} + } + } + }, + 'definitions': { + 'exchange': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'key': {'type': 'string'}, + 'secret': {'type': 'string'}, + 'pair_whitelist': { + 'type': 'array', + 'items': { + 'type': 'string', + 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' + }, + 'uniqueItems': True + }, + 'pair_blacklist': { + 'type': 'array', + 'items': { + 'type': 'string', + 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' + }, + 'uniqueItems': True + } + }, + 'required': ['name', 'key', 'secret', 'pair_whitelist'] + } + }, + 'anyOf': [ + {'required': ['exchange']} + ], + 'required': [ + 'max_open_trades', + 'stake_currency', + 'stake_amount', + 'fiat_display_currency', + 'dry_run', + 'bid_strategy', + 'telegram' + ] +} diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f0bf3fa4d..0a332b952 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,20 +11,19 @@ from typing import Dict, List, Optional, Any, Callable import arrow import requests -from cachetools import cached, TTLCache +from cachetools import TTLCache, cached from freqtrade import ( DependencyException, OperationalException, TemporaryError, exchange, persistence, __version__, ) +from freqtrade import constants from freqtrade.analyze import Analyze -from freqtrade.constants import Constants from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.persistence import Trade from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.state import State - logger = logging.getLogger(__name__) @@ -111,7 +110,7 @@ class FreqtradeBot(object): elif state == State.RUNNING: min_secs = self.config.get('internals', {}).get( 'process_throttle_secs', - Constants.PROCESS_THROTTLE_SECS + constants.PROCESS_THROTTLE_SECS ) nb_assets = self.config.get('dynamic_whitelist', None) @@ -175,7 +174,7 @@ class FreqtradeBot(object): except TemporaryError as error: logger.warning('%s, retrying in 30 seconds...', error) - time.sleep(Constants.RETRY_TIMEOUT) + time.sleep(constants.RETRY_TIMEOUT) except OperationalException: self.rpc.send_msg( '*Status:* OperationalException:\n```\n{traceback}```{hint}' @@ -447,7 +446,7 @@ class FreqtradeBot(object): if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell): self.execute_sell(trade, current_rate) return True - + logger.info('Found no sell signals for whitelisted currencies. Trying again..') return False def check_handle_timedout(self, timeoutvalue: int) -> None: diff --git a/freqtrade/main.py b/freqtrade/main.py index 93d7e5c54..9639922f9 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -3,7 +3,6 @@ Main Freqtrade bot script. Read the documentation to know what cli arguments you need. """ - import logging import sys from typing import List @@ -30,9 +29,10 @@ def main(sysargv: List[str]) -> None: # Means if Backtesting or Hyperopt have been called we exit the bot if hasattr(args, 'func'): args.func(args) - return 0 + return freqtrade = None + return_code = 1 try: # Load and validate configuration config = Configuration(args).get_config() @@ -46,12 +46,13 @@ def main(sysargv: List[str]) -> None: except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') + return_code = 0 except BaseException: logger.exception('Fatal exception!') finally: if freqtrade: freqtrade.clean() - sys.exit(0) + sys.exit(return_code) def set_loggers() -> None: diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 98267ce77..761b8cc4c 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -113,45 +113,38 @@ def download_pairs(datadir, pairs: List[str], ticker_interval: str) -> bool: # FIX: 20180110, suggest rename interval to tick_interval -def download_backtesting_testdata(datadir: str, pair: str, interval: str = '5m') -> bool: +def download_backtesting_testdata(datadir: str, pair: str, interval: str = '5m') -> None: """ Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters Based on @Rybolov work: https://github.com/rybolov/freqtrade-data - :param pairs: list of pairs to download - :return: bool """ path = make_testdata_path(datadir) logger.info( - 'Download the pair: "%s", Interval: %s', - pair, - interval + 'Download the pair: "%s", Interval: %s', pair, interval ) - filepair = pair.replace("/", "_") filename = os.path.join(path, '{pair}-{interval}.json'.format( - pair=filepair, + pair=pair.replace("/", "_"), interval=interval, )) if os.path.isfile(filename): with open(filename, "rt") as file: data = json.load(file) - logger.debug("Current Start: %s", misc.format_ms_time(data[1][0])) - logger.debug("Current End: %s", misc.format_ms_time(data[-1:][0][0])) else: data = [] - logger.debug("Current Start: None") - logger.debug("Current End: None") - new_data = get_ticker_history(pair=pair, tick_interval=interval) - for row in new_data: - if row not in data: - data.append(row) - logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) - logger.debug("New End: %s", misc.format_ms_time(data[-1:][0][0])) - data = sorted(data, key=lambda data: data[0]) + logger.debug('Current Start: %s', data[0][0] if data else None) + logger.debug('Current End: %s', data[-1:][0][0] if data else None) + # Extend data with new ticker history + data.extend([ + row for row in get_ticker_history(pair=pair, tick_interval=interval) + if row not in data + ]) + + data = sorted(data, key=lambda _data: _data[0]) + logger.debug('New Start: %s', data[0][0]) + logger.debug('New End: %s', data[-1:][0][0]) misc.file_dump_json(filename, data) - - return True diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6df4a99fa..376730d0f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -4,11 +4,12 @@ This module contains the backtesting logic """ import logging +import operator from argparse import Namespace from typing import Dict, Tuple, Any, List, Optional import arrow -from pandas import DataFrame, Series +from pandas import DataFrame from tabulate import tabulate import freqtrade.optimize as optimize @@ -19,7 +20,6 @@ from freqtrade.configuration import Configuration from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade - logger = logging.getLogger(__name__) @@ -66,11 +66,12 @@ class Backtesting(object): :param data: dictionary with preprocessed backtesting data :return: tuple containing min_date, max_date """ - all_dates = Series([]) - for pair_data in data.values(): - all_dates = all_dates.append(pair_data['date']) - all_dates.sort_values(inplace=True) - return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1]) + timeframe = [ + (arrow.get(min(frame.date)), arrow.get(max(frame.date))) + for frame in data.values() + ] + return min(timeframe, key=operator.itemgetter(0))[0], \ + max(timeframe, key=operator.itemgetter(1))[1] def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str: """ @@ -201,9 +202,9 @@ class Backtesting(object): # record a tuple of pair, current_profit_percent, # entry-date, duration records.append((pair, trade_entry[1], - row.date.timestamp(), - row2.date.timestamp(), - row.date, trade_entry[3])) + row.date.strftime('%s'), + row2.date.strftime('%s'), + index, trade_entry[3])) # For now export inside backtest(), maybe change so that backtest() # returns a tuple like: (dataframe, records, logs, etc) if record and record.find('trades') >= 0: @@ -304,12 +305,9 @@ def start(args: Namespace) -> None: :param args: Cli args from Arguments() :return: None """ - - # Initialize logger - logger.info('Starting freqtrade in Backtesting mode') - # Initialize configuration config = setup_configuration(args) + logger.info('Starting freqtrade in Backtesting mode') # Initialize backtesting object backtesting = Backtesting(config) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3bdd47eb6..20fa5380d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -29,7 +29,6 @@ from freqtrade.optimize import load_data from freqtrade.optimize.backtesting import Backtesting from user_data.hyperopt_conf import hyperopt_optimize_conf - logger = logging.getLogger(__name__) @@ -591,11 +590,11 @@ def start(args: Namespace) -> None: logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING) logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) - logger.info('Starting freqtrade in Hyperopt mode') - # Initialize configuration # Monkey patch the configuration with hyperopt_conf.py configuration = Configuration(args) + logger.info('Starting freqtrade in Hyperopt mode') + optimize_config = hyperopt_optimize_conf() config = configuration._load_common_config(optimize_config) config = configuration._load_backtesting_config(config) diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index ec39578c7..22689f17a 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -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): """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4eb73fb2e..dcf665a02 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -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 """ diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py new file mode 100644 index 000000000..8f4972919 --- /dev/null +++ b/freqtrade/strategy/resolver.py @@ -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 diff --git a/freqtrade/strategy/strategy.py b/freqtrade/strategy/strategy.py deleted file mode 100644 index 0faa64a05..000000000 --- a/freqtrade/strategy/strategy.py +++ /dev/null @@ -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) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index a3185e63b..bb42bcff9 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -12,7 +12,7 @@ from sqlalchemy import create_engine from telegram import Chat, Message, Update from freqtrade.analyze import Analyze -from freqtrade.constants import Constants +from freqtrade import constants from freqtrade.freqtradebot import FreqtradeBot logging.getLogger('').setLevel(logging.INFO) @@ -87,7 +87,7 @@ def default_conf(): "initial_state": "running", "loglevel": logging.DEBUG } - validate(configuration, Constants.CONF_SCHEMA) + validate(configuration, constants.CONF_SCHEMA) return configuration @@ -302,7 +302,7 @@ def ticker_history(): 0.05874751, ], [ - 1511686800, + 1511686800000, 8.891e-05, 8.893e-05, 8.875e-05, diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 7b6783d92..f17a0115e 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -169,7 +169,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'backtesting' ] @@ -210,7 +210,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', 'backtesting', '--ticker-interval', '1m', @@ -274,7 +274,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: )) args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'backtesting' ] args = get_args(args) @@ -612,12 +612,12 @@ def test_backtest_start_live(default_conf, mocker, caplog): args.live = True args.datadir = None args.export = None - args.strategy = 'default_strategy' + args.strategy = 'DefaultStrategy' args.timerange = '-100' # needed due to MagicMock malleability args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'backtesting', '--ticker-interval', '1m', '--live', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 4916c1040..68fc99955 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -3,17 +3,16 @@ import os import signal from copy import deepcopy from unittest.mock import MagicMock -import pytest import pandas as pd +import pytest from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.optimize.hyperopt import Hyperopt, start -from freqtrade.strategy.strategy import Strategy +from freqtrade.strategy.resolver import StrategyResolver from freqtrade.tests.conftest import log_has from freqtrade.tests.optimize.test_backtesting import get_args - # Avoid to reinit the same object again and again _HYPEROPT_INITIALIZED = False _HYPEROPT = None @@ -71,12 +70,12 @@ def test_start(mocker, default_conf, caplog) -> None: args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'hyperopt', '--epochs', '5' ] args = get_args(args) - Strategy({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) start(args) import pprint @@ -94,7 +93,7 @@ def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None: Test Hyperopt.calculate_loss() """ hyperopt = _HYPEROPT - Strategy({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) @@ -124,7 +123,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None: assert under > correct -def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None: +def test_log_results_if_loss_improves(capsys) -> None: hyperopt = _HYPEROPT hyperopt.current_best_loss = 2 hyperopt.log_results( @@ -186,7 +185,7 @@ def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None: mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) - Strategy({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) hyperopt.trials = create_trials(mocker) hyperopt.tickerdata_to_dataframe = MagicMock() @@ -231,7 +230,7 @@ def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) - Strategy({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) hyperopt.trials = create_trials(mocker) hyperopt.tickerdata_to_dataframe = MagicMock() @@ -274,7 +273,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, defa mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf) mocker.patch('freqtrade.exchange.validate_pairs', MagicMock()) - Strategy({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) hyperopt.trials = trials hyperopt.tickerdata_to_dataframe = MagicMock() diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 411714ec8..38d6a76ec 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -186,10 +186,11 @@ def test_download_backtesting_testdata2(mocker) -> None: [1509836520000, 0.00162008, 0.00162008, 0.00162008, 0.00162008, 108.14853839], [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] ] - mocker.patch('freqtrade.misc.file_dump_json', return_value=None) + json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick) - assert download_backtesting_testdata(None, pair="UNITTEST/BTC", interval='1m') - assert download_backtesting_testdata(None, pair="UNITTEST/BTC", interval='3m') + download_backtesting_testdata(None, pair="UNITTEST/BTC", interval='1m') + download_backtesting_testdata(None, pair="UNITTEST/BTC", interval='3m') + assert json_dump_mock.call_count == 2 def test_load_tickerdata_file() -> None: diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py index cb6cbd4ca..900fc2234 100644 --- a/freqtrade/tests/strategy/test_default_strategy.py +++ b/freqtrade/tests/strategy/test_default_strategy.py @@ -1,10 +1,16 @@ +import json + +import pytest from pandas import DataFrame -from freqtrade.strategy.default_strategy import DefaultStrategy, class_name +from freqtrade.analyze import Analyze +from freqtrade.strategy.default_strategy import DefaultStrategy -def test_default_strategy_class_name(): - assert class_name == DefaultStrategy.__name__ +@pytest.fixture +def result(): + with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file: + return Analyze.parse_ticker_dataframe(json.load(data_file)) def test_default_strategy_structure(): diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index a364a59fe..244910790 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,89 +1,85 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 import logging +import os -from freqtrade.strategy.strategy import Strategy +import pytest - -def test_sanitize_module_name(): - assert Strategy._sanitize_module_name('default_strategy') == 'default_strategy' - assert Strategy._sanitize_module_name('default_strategy.py') == 'default_strategy' - assert Strategy._sanitize_module_name('../default_strategy.py') == 'default_strategy' - assert Strategy._sanitize_module_name('../default_strategy') == 'default_strategy' - assert Strategy._sanitize_module_name('.default_strategy') == '.default_strategy' - assert Strategy._sanitize_module_name('foo-bar') == 'foo-bar' - assert Strategy._sanitize_module_name('foo/bar') == 'bar' +from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy.resolver import StrategyResolver def test_search_strategy(): - assert Strategy._search_strategy('default_strategy') == '.' - assert Strategy._search_strategy('test_strategy') == 'user_data.strategies.' - assert Strategy._search_strategy('super_duper') is None - - -def test_strategy_structure(): - assert hasattr(Strategy, 'populate_indicators') - assert hasattr(Strategy, 'populate_buy_trend') - assert hasattr(Strategy, 'populate_sell_trend') + default_location = os.path.join(os.path.dirname( + os.path.realpath(__file__)), '..', '..', 'strategy' + ) + assert isinstance( + StrategyResolver._search_strategy(default_location, 'DefaultStrategy'), IStrategy + ) + assert StrategyResolver._search_strategy(default_location, 'NotFoundStrategy') is None def test_load_strategy(result): - strategy = Strategy() - - assert not hasattr(Strategy, 'custom_strategy') - strategy._load_strategy('test_strategy') - - assert not hasattr(Strategy, 'custom_strategy') - - assert hasattr(strategy.custom_strategy, 'populate_indicators') - assert 'adx' in strategy.populate_indicators(result) + resolver = StrategyResolver() + resolver._load_strategy('TestStrategy') + assert hasattr(resolver.strategy, 'populate_indicators') + assert 'adx' in resolver.strategy.populate_indicators(result) -def test_load_not_found_strategy(caplog): - strategy = Strategy() +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 not hasattr(Strategy, 'custom_strategy') - strategy._load_strategy('NotFoundStrategy') + assert hasattr(resolver.strategy, 'populate_indicators') + assert 'adx' in resolver.strategy.populate_indicators(result) - error_msg = "Impossible to load Strategy 'user_data/strategies/{}.py'. This file does not " \ - "exist or contains Python code errors".format('NotFoundStrategy') - assert ('freqtrade.strategy.strategy', logging.ERROR, error_msg) in caplog.record_tuples + +def test_load_not_found_strategy(): + strategy = StrategyResolver() + with pytest.raises(ImportError, + match=r'Impossible to load Strategy \'NotFoundStrategy\'.' + r' This class does not exist or contains Python code errors'): + strategy._load_strategy('NotFoundStrategy') def test_strategy(result): - strategy = Strategy({'strategy': 'default_strategy'}) + resolver = StrategyResolver({'strategy': 'DefaultStrategy'}) - assert hasattr(strategy.custom_strategy, 'minimal_roi') - assert strategy.minimal_roi[0] == 0.04 + assert hasattr(resolver.strategy, 'minimal_roi') + assert resolver.strategy.minimal_roi[0] == 0.04 - assert hasattr(strategy.custom_strategy, 'stoploss') - assert strategy.stoploss == -0.10 + assert hasattr(resolver.strategy, 'stoploss') + assert resolver.strategy.stoploss == -0.10 - assert hasattr(strategy.custom_strategy, 'populate_indicators') - assert 'adx' in strategy.populate_indicators(result) + assert hasattr(resolver.strategy, 'populate_indicators') + assert 'adx' in resolver.strategy.populate_indicators(result) - assert hasattr(strategy.custom_strategy, 'populate_buy_trend') - dataframe = strategy.populate_buy_trend(strategy.populate_indicators(result)) + assert hasattr(resolver.strategy, 'populate_buy_trend') + dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result)) assert 'buy' in dataframe.columns - assert hasattr(strategy.custom_strategy, 'populate_sell_trend') - dataframe = strategy.populate_sell_trend(strategy.populate_indicators(result)) + assert hasattr(resolver.strategy, 'populate_sell_trend') + dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result)) assert 'sell' in dataframe.columns def test_strategy_override_minimal_roi(caplog): caplog.set_level(logging.INFO) config = { - 'strategy': 'default_strategy', + 'strategy': 'DefaultStrategy', 'minimal_roi': { "0": 0.5 } } - strategy = Strategy(config) + resolver = StrategyResolver(config) - assert hasattr(strategy.custom_strategy, 'minimal_roi') - assert strategy.minimal_roi[0] == 0.5 - assert ('freqtrade.strategy.strategy', + assert hasattr(resolver.strategy, 'minimal_roi') + assert resolver.strategy.minimal_roi[0] == 0.5 + assert ('freqtrade.strategy.resolver', logging.INFO, 'Override strategy \'minimal_roi\' with value in config file.' ) in caplog.record_tuples @@ -92,14 +88,14 @@ def test_strategy_override_minimal_roi(caplog): def test_strategy_override_stoploss(caplog): caplog.set_level(logging.INFO) config = { - 'strategy': 'default_strategy', + 'strategy': 'DefaultStrategy', 'stoploss': -0.5 } - strategy = Strategy(config) + resolver = StrategyResolver(config) - assert hasattr(strategy.custom_strategy, 'stoploss') - assert strategy.stoploss == -0.5 - assert ('freqtrade.strategy.strategy', + assert hasattr(resolver.strategy, 'stoploss') + assert resolver.strategy.stoploss == -0.5 + assert ('freqtrade.strategy.resolver', logging.INFO, 'Override strategy \'stoploss\' with value in config file: -0.5.' ) in caplog.record_tuples @@ -109,34 +105,14 @@ def test_strategy_override_ticker_interval(caplog): caplog.set_level(logging.INFO) config = { - 'strategy': 'default_strategy', + 'strategy': 'DefaultStrategy', 'ticker_interval': 60 } - strategy = Strategy(config) + resolver = StrategyResolver(config) - assert hasattr(strategy.custom_strategy, 'ticker_interval') - assert strategy.ticker_interval == 60 - assert ('freqtrade.strategy.strategy', + assert hasattr(resolver.strategy, 'ticker_interval') + assert resolver.strategy.ticker_interval == 60 + assert ('freqtrade.strategy.resolver', logging.INFO, 'Override strategy \'ticker_interval\' with value in config file: 60.' ) in caplog.record_tuples - - -def test_strategy_fallback_default_strategy(): - strategy = Strategy() - strategy.logger = logging.getLogger(__name__) - - assert not hasattr(Strategy, 'custom_strategy') - strategy._load_strategy('../../super_duper') - assert not hasattr(Strategy, 'custom_strategy') - - -def test_strategy_singleton(): - strategy1 = Strategy({'strategy': 'default_strategy'}) - - assert hasattr(strategy1.custom_strategy, 'minimal_roi') - assert strategy1.minimal_roi[0] == 0.04 - - strategy2 = Strategy() - assert hasattr(strategy2.custom_strategy, 'minimal_roi') - assert strategy2.minimal_roi[0] == 0.04 diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 8d521150d..01033ce7d 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -16,7 +16,7 @@ from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.tests.conftest import log_has # Avoid to reinit the same object again and again -_ANALYZE = Analyze({'strategy': 'default_strategy'}) +_ANALYZE = Analyze({'strategy': 'DefaultStrategy'}) def test_signaltype_object() -> None: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 881129887..0e7bf3933 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -71,6 +71,26 @@ def test_parse_args_invalid() -> None: 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: args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg() assert args.dynamic_whitelist == 20 diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index c45fa94c8..da7a042f5 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -99,8 +99,8 @@ def test_load_config(default_conf, mocker) -> None: configuration = Configuration(args) validated_conf = configuration.load_config() - assert 'strategy' in validated_conf - assert validated_conf['strategy'] == 'default_strategy' + assert validated_conf.get('strategy') == 'DefaultStrategy' + assert validated_conf.get('strategy_path') is None assert 'dynamic_whitelist' not in validated_conf assert 'dry_run_db' not in validated_conf @@ -115,20 +115,40 @@ def test_load_config_with_params(default_conf, mocker) -> None: args = [ '--dynamic-whitelist', '10', - '--strategy', 'test_strategy', - '--dry-run-db' + '--strategy', 'TestStrategy', + '--strategy-path', '/some/path', + '--dry-run-db', ] args = Arguments(args, '').get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() - assert 'dynamic_whitelist' in validated_conf - assert validated_conf['dynamic_whitelist'] == 10 - assert 'strategy' in validated_conf - assert validated_conf['strategy'] == 'test_strategy' - assert 'dry_run_db' in validated_conf - assert validated_conf['dry_run_db'] is True + assert validated_conf.get('dynamic_whitelist') == 10 + assert validated_conf.get('strategy') == 'TestStrategy' + assert validated_conf.get('strategy_path') == '/some/path' + assert validated_conf.get('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: @@ -141,7 +161,7 @@ def test_show_info(default_conf, mocker, caplog) -> None: args = [ '--dynamic-whitelist', '10', - '--strategy', 'test_strategy', + '--strategy', 'TestStrategy', '--dry-run-db' ] args = Arguments(args, '').get_parsed_arg() @@ -185,7 +205,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'backtesting' ] @@ -229,7 +249,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', 'backtesting', '--ticker-interval', '1m', diff --git a/freqtrade/tests/test_constants.py b/freqtrade/tests/test_constants.py index 6d544502f..541c6e533 100644 --- a/freqtrade/tests/test_constants.py +++ b/freqtrade/tests/test_constants.py @@ -2,25 +2,24 @@ Unit test file for constants.py """ -from freqtrade.constants import Constants +from freqtrade import constants def test_constant_object() -> None: """ Test the Constants object has the mandatory Constants """ - assert hasattr(Constants, 'CONF_SCHEMA') - assert hasattr(Constants, 'DYNAMIC_WHITELIST') - assert hasattr(Constants, 'PROCESS_THROTTLE_SECS') - assert hasattr(Constants, 'TICKER_INTERVAL') - assert hasattr(Constants, 'HYPEROPT_EPOCH') - assert hasattr(Constants, 'RETRY_TIMEOUT') - assert hasattr(Constants, 'DEFAULT_STRATEGY') + assert hasattr(constants, 'CONF_SCHEMA') + assert hasattr(constants, 'DYNAMIC_WHITELIST') + assert hasattr(constants, 'PROCESS_THROTTLE_SECS') + assert hasattr(constants, 'TICKER_INTERVAL') + assert hasattr(constants, 'HYPEROPT_EPOCH') + assert hasattr(constants, 'RETRY_TIMEOUT') + assert hasattr(constants, 'DEFAULT_STRATEGY') def test_conf_schema() -> None: """ Test the CONF_SCHEMA is from the right type """ - constant = Constants() - assert isinstance(constant.CONF_SCHEMA, dict) + assert isinstance(constants.CONF_SCHEMA, dict) diff --git a/freqtrade/tests/test_dataframe.py b/freqtrade/tests/test_dataframe.py index 47d89a112..fd461a503 100644 --- a/freqtrade/tests/test_dataframe.py +++ b/freqtrade/tests/test_dataframe.py @@ -4,7 +4,7 @@ import pandas from freqtrade.analyze import Analyze from freqtrade.optimize import load_data -from freqtrade.strategy.strategy import Strategy +from freqtrade.strategy.resolver import StrategyResolver _pairs = ['ETH/BTC'] @@ -15,19 +15,19 @@ def load_dataframe_pair(pairs): assert isinstance(pairs[0], str) dataframe = ld[pairs[0]] - analyze = Analyze({'strategy': 'default_strategy'}) + analyze = Analyze({'strategy': 'DefaultStrategy'}) dataframe = analyze.analyze_ticker(dataframe) return dataframe def test_dataframe_load(): - Strategy({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) dataframe = load_dataframe_pair(_pairs) assert isinstance(dataframe, pandas.core.frame.DataFrame) def test_dataframe_columns_exists(): - Strategy({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) dataframe = load_dataframe_pair(_pairs) assert 'high' in dataframe.columns assert 'low' in dataframe.columns diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 44c9db73d..1c6723043 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -42,13 +42,11 @@ def test_datesarray_to_datetimearray(ticker_history): assert date_len == 3 -def test_common_datearray(default_conf, mocker) -> None: +def test_common_datearray(default_conf) -> None: """ Test common_datearray() :return: None """ - mocker.patch('freqtrade.strategy.strategy.Strategy', MagicMock()) - analyze = Analyze(default_conf) tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': tick} diff --git a/requirements.txt b/requirements.txt index 6295164bb..4aa22fcd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ ccxt==1.11.149 -SQLAlchemy==1.2.5 -python-telegram-bot==10.0.1 +SQLAlchemy==1.2.7 +python-telegram-bot==10.0.2 arrow==0.12.1 cachetools==2.0.1 requests==2.18.4 @@ -8,12 +8,12 @@ urllib3==1.22 wrapt==1.10.11 pandas==0.22.0 scikit-learn==0.19.1 -scipy==1.0.0 +scipy==1.0.1 jsonschema==2.6.0 -numpy==1.14.2 +numpy==1.14.3 TA-Lib==0.4.17 -pytest==3.5.0 -pytest-mock==1.7.1 +pytest==3.5.1 +pytest-mock==1.10.0 pytest-cov==2.5.1 hyperopt==0.1 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 136484af2..5e533a030 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -27,7 +27,11 @@ from freqtrade import exchange import freqtrade.optimize as optimize +<<<<<<< HEAD logger = logging.getLogger('freqtrade') +======= +logger = logging.getLogger(__name__) +>>>>>>> bddf009a2b6d0e1a19cca558887ce972e99a6238 def plot_analyzed_dataframe(args: Namespace) -> None: diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 8e121deea..19cf8b11a 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -24,13 +24,20 @@ import plotly.graph_objs as go from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.analyze import Analyze +<<<<<<< HEAD from freqtrade.constants import Constants +======= +>>>>>>> bddf009a2b6d0e1a19cca558887ce972e99a6238 import freqtrade.optimize as optimize import freqtrade.misc as misc +<<<<<<< HEAD logger = logging.getLogger('freqtrade') +======= +logger = logging.getLogger(__name__) +>>>>>>> bddf009a2b6d0e1a19cca558887ce972e99a6238 # data:: [ pair, profit-%, enter, exit, time, duration] diff --git a/setup.sh b/setup.sh index b38f2c645..bdcec7186 100755 --- a/setup.sh +++ b/setup.sh @@ -117,7 +117,7 @@ function config_generator () { -e "s/\"your_exchange_key\"/\"$api_key\"/g" \ -e "s/\"your_exchange_secret\"/\"$api_secret\"/g" \ -e "s/\"your_telegram_token\"/\"$token\"/g" \ - -e "s/\"your_telegram_chat_id\"/\"$chat_id\"/g" + -e "s/\"your_telegram_chat_id\"/\"$chat_id\"/g" \ -e "s/\"dry_run\": false,/\"dry_run\": true,/g" config.json.example > config.json } @@ -205,4 +205,4 @@ plot help ;; esac -exit 0 \ No newline at end of file +exit 0 diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index a164812c4..4ba1dbe17 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -10,10 +10,6 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib import numpy # noqa -# Update this variable if you change the class name -class_name = 'TestStrategy' - - # This class is a sample. Feel free to customize it. class TestStrategy(IStrategy): """