diff --git a/.coveragerc b/.coveragerc index 96ad6b09b..74dccbfe1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,7 @@ [run] omit = scripts/* + freqtrade/templates/* freqtrade/vendor/* freqtrade/__main__.py tests/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99f3b3eec..f6a111944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,19 +73,21 @@ jobs: # Allow failure for coveralls # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" + export TRAVIS_BRANCH=${GITHUB_REF#"ref/heads"} export CI_BRANCH=${GITHUB_REF#"ref/heads"} - export CI_BRANCH=${HEAD_REF} - echo "${CI_BRANCH}" + echo "${TRAVIS_BRANCH}" coveralls || true - name: Backtesting run: | cp config.json.example config.json - freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy + freqtrade create-userdir --userdir user_data + freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - name: Hyperopt run: | cp config.json.example config.json + freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt - name: Flake8 @@ -142,11 +144,13 @@ jobs: - name: Backtesting run: | cp config.json.example config.json - freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy + freqtrade create-userdir --userdir user_data + freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - name: Hyperopt run: | cp config.json.example config.json + freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt - name: Flake8 diff --git a/.travis.yml b/.travis.yml index 6073e1cce..ec688a1f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,10 +28,12 @@ jobs: name: pytest - script: - cp config.json.example config.json - - freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy + - freqtrade create-userdir --userdir user_data + - freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy name: backtest - script: - cp config.json.example config.json + - freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt name: hyperopt - script: flake8 diff --git a/docs/developer.md b/docs/developer.md index ab647726c..d731f1768 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -200,8 +200,8 @@ If the day shows the same day, then the last candle can be assumed as incomplete To keep the jupyter notebooks aligned with the documentation, the following should be ran after updating a example notebook. ``` bash -jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace user_data/notebooks/strategy_analysis_example.ipynb -jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md +jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace freqtrade/templates/strategy_analysis_example.ipynb +jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md ``` ## Continuous integration diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6c1505e75..5a3ae7e3a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -15,10 +15,13 @@ To learn how to get data for the pairs and exchange you're interrested in, head ## Prepare Hyperopting Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py). +the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. +The simplest way to get started is to use `freqtrade new-hyperopt --hyperopt AwesomeHyperopt`. +This will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + ### Checklist on all tasks / possibilities in hyperopt Depending on the space you want to optimize, only some of the below are required: @@ -423,7 +426,7 @@ These ranges should be sufficient in most cases. The minutes in the steps (ROI d If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). ### Understand Hyperopt Stoploss results @@ -458,7 +461,7 @@ If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimiza If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). ### Validate backtesting results diff --git a/docs/installation.md b/docs/installation.md index 593da2acf..411441aa2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -162,7 +162,7 @@ Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git - +cd freqtrade ``` Optionally checkout the master branch to get the latest stable release: @@ -171,22 +171,24 @@ Optionally checkout the master branch to get the latest stable release: git checkout master ``` -#### 4. Initialize the configuration - -```bash -cd freqtrade -cp config.json.example config.json -``` - -> *To edit the config please refer to [Bot Configuration](configuration.md).* - -#### 5. Install python dependencies +#### 4. Install python dependencies ``` bash python3 -m pip install --upgrade pip python3 -m pip install -e . ``` +#### 5. Initialize the configuration + +```bash +# Initialize the user_directory +freqtrade create-userdir --userdir user_data/ + +cp config.json.example config.json +``` + +> *To edit the config please refer to [Bot Configuration](configuration.md).* + #### 6. Run the Bot If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. @@ -227,7 +229,7 @@ If that is not available on your system, feel free to try the instructions below Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. !!! Hint - Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document. + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. #### Clone the git repository diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 03aecd6ba..c43d8e3f6 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -7,24 +7,28 @@ indicators. This is very simple. Copy paste your strategy file into the directory `user_data/strategies`. -Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: +Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`: -1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py` +1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py` 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) ```bash freqtrade trade --strategy AwesomeStrategy ``` -## Change your strategy +## Develop your own strategy -The bot includes a default strategy file. However, we recommend you to -use your own file to not have to lose your parameters every time the default -strategy file will be updated on Github. Put your custom strategy file -into the directory `user_data/strategies`. +The bot includes a default strategy file. +Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). -Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. -`cp user_data/strategies/sample_strategy.py user_data/strategies/awesome-strategy.py` +You will however most likely have your own idea for a strategy. +This document intends to help you develop one for yourself. + +To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`. +This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`. + +!!! Note + This is just a template file, which will most likely not be profitable out of the box. ### Anatomy of a strategy @@ -48,7 +52,7 @@ Future versions will require this to be set. freqtrade trade --strategy AwesomeStrategy ``` -**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py) +**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py) file as reference.** !!! Note "Strategies and Backtesting" @@ -114,7 +118,7 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame ``` !!! Note "Want more indicator examples?" - Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). + Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). Then uncomment indicators you need. ### Strategy startup period @@ -478,7 +482,7 @@ Printing more than a few rows is also possible (simply use `print(dataframe)` i ### Where can i find a strategy template? The strategy template is located in the file -[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). +[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). ### Specify custom strategy location diff --git a/docs/utils.md b/docs/utils.md index 9f5792660..ca4b645a5 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -2,6 +2,116 @@ Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section. +## Create userdir + +Creates the directory structure to hold your files for freqtrade. +Will also create strategy and hyperopt examples for you to get started. +Can be used multiple times - using `--reset` will reset the sample strategy and hyperopt files to their default state. + +``` +usage: freqtrade create-userdir [-h] [--userdir PATH] [--reset] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + --reset Reset sample files to their original state. +``` + +!!! Warning + Using `--reset` may result in loss of data, since this will overwrite all sample files without asking again. + +``` +├── backtest_results +├── data +├── hyperopt_results +├── hyperopts +│   ├── sample_hyperopt_advanced.py +│   ├── sample_hyperopt_loss.py +│   └── sample_hyperopt.py +├── notebooks +│   └── strategy_analysis_example.ipynb +├── plot +└── strategies + └── sample_strategy.py +``` + +## Create new strategy + +Creates a new strategy from a template similar to SampleStrategy. +The file will be named inline with your class name, and will not overwrite existing files. + +Results will be located in `user_data/strategies/.py`. + +### Sample usage of new-strategy + +```bash +freqtrade new-strategy --strategy AwesomeStrategy +``` + +With custom user directory + +```bash +freqtrade new-strategy --userdir ~/.freqtrade/ --strategy AwesomeStrategy +``` + +### new-strategy complete options + +``` output +usage: freqtrade new-strategy [-h] [--userdir PATH] [-s NAME] + [--template {full,minimal}] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --template {full,minimal} + Use a template which is either `minimal` or `full` + (containing multiple sample indicators). Default: + `full`. + +``` + +## Create new hyperopt + +Creates a new hyperopt from a template similar to SampleHyperopt. +The file will be named inline with your class name, and will not overwrite existing files. + +Results will be located in `user_data/hyperopts/.py`. + +### Sample usage of new-hyperopt + +```bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +With custom user directory + +```bash +freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt +``` + +### new-hyperopt complete options + +``` output +usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME] + [--template {full,minimal}] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + --hyperopt NAME Specify hyperopt class name which will be used by the + bot. + --template {full,minimal} + Use a template which is either `minimal` or `full` + (containing multiple sample indicators). Default: + `full`. +``` + ## List Exchanges Use the `list-exchanges` subcommand to see the exchanges available for the bot. diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index fa1d77407..e4e051b52 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -37,7 +37,11 @@ ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] -ARGS_CREATE_USERDIR = ["user_data_dir"] +ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] + +ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] + +ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase"] @@ -58,7 +62,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", "hyperopt_list", "hyperopt_show", "plot-dataframe", "plot-profit"] -NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"] +NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"] class Arguments: @@ -124,6 +128,7 @@ class Arguments: from freqtrade.utils import (start_create_userdir, start_download_data, start_hyperopt_list, start_hyperopt_show, start_list_exchanges, start_list_markets, + start_new_hyperopt, start_new_strategy, start_list_timeframes, start_trading) from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit @@ -165,6 +170,18 @@ class Arguments: create_userdir_cmd.set_defaults(func=start_create_userdir) self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd) + # add new-strategy subcommand + build_strategy_cmd = subparsers.add_parser('new-strategy', + help="Create new strategy") + build_strategy_cmd.set_defaults(func=start_new_strategy) + self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd) + + # add new-hyperopt subcommand + build_hyperopt_cmd = subparsers.add_parser('new-hyperopt', + help="Create new hyperopt") + build_hyperopt_cmd.set_defaults(func=start_new_hyperopt) + self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd) + # Add list-exchanges subcommand list_exchanges_cmd = subparsers.add_parser( 'list-exchanges', diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index beed2b835..1dcbd1f96 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -74,6 +74,11 @@ AVAILABLE_CLI_OPTIONS = { help='Path to userdata directory.', metavar='PATH', ), + "reset": Arg( + '--reset', + help='Reset sample files to their original state.', + action='store_true', + ), # Main options "strategy": Arg( '-s', '--strategy', @@ -346,6 +351,14 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), + # Templating options + "template": Arg( + '--template', + help='Use a template which is either `minimal` or ' + '`full` (containing multiple sample indicators). Default: `%(default)s`.', + choices=['full', 'minimal'], + default='full', + ), # Plot dataframe "indicators1": Arg( '--indicators1', diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 8f3dbd675..b1e3535a3 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -58,6 +58,13 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', 'experimental', 'ignore_roi_if_buy_signal') + if not config.get('pairlists') and not config.get('pairlists'): + config['pairlists'] = [{'method': 'StaticPairList'}] + logger.warning( + "DEPRECATED: " + "Pairlists must be defined explicitly in the future." + "Defaulting to StaticPairList for now.") + if config.get('pairlist', {}).get("method") == 'VolumePairList': logger.warning( "DEPRECATED: " diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 395accd90..3dd76a025 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -1,8 +1,10 @@ import logging -from typing import Any, Dict, Optional +import shutil from pathlib import Path +from typing import Any, Dict, Optional from freqtrade import OperationalException +from freqtrade.constants import USER_DATA_FILES logger = logging.getLogger(__name__) @@ -31,7 +33,8 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path: :param create_dir: Create directory if it does not exist. :return: Path object containing the directory """ - sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "plot", "strategies", ] + sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "notebooks", + "plot", "strategies", ] folder = Path(directory) if not folder.is_dir(): if create_dir: @@ -48,3 +51,26 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path: if not subfolder.is_dir(): subfolder.mkdir(parents=False) return folder + + +def copy_sample_files(directory: Path, overwrite: bool = False) -> None: + """ + Copy files from templates to User data directory. + :param directory: Directory to copy data to + :param overwrite: Overwrite existing sample files + """ + if not directory.is_dir(): + raise OperationalException(f"Directory `{directory}` does not exist.") + sourcedir = Path(__file__).parents[1] / "templates" + for source, target in USER_DATA_FILES.items(): + targetdir = directory / target + if not targetdir.is_dir(): + raise OperationalException(f"Directory `{targetdir}` does not exist.") + targetfile = targetdir / source + if targetfile.exists(): + if not overwrite: + logger.warning(f"File `{targetfile}` exists already, not deploying sample file.") + continue + else: + logger.warning(f"File `{targetfile}` exists already, overwriting.") + shutil.copy(str(sourcedir / source), str(targetfile)) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a92371bc3..bf5d822c6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -22,6 +22,18 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'P DRY_RUN_WALLET = 999.9 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons +USERPATH_HYPEROPTS = 'hyperopts' +USERPATH_STRATEGY = 'strategies' + +# Soure files with destination directories within user-directory +USER_DATA_FILES = { + 'sample_strategy.py': USERPATH_STRATEGY, + 'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS, + 'sample_hyperopt_loss.py': USERPATH_HYPEROPTS, + 'sample_hyperopt.py': USERPATH_HYPEROPTS, + 'strategy_analysis_example.ipynb': 'notebooks', +} + TIMEFRAMES = [ '1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 7682b5285..bcba78cf0 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -127,3 +127,16 @@ def round_dict(d, n): def plural(num, singular: str, plural: str = None) -> str: return singular if (num == 1 or num == -1) else plural or singular + 's' + + +def render_template(templatefile: str, arguments: dict = {}): + + from jinja2 import Environment, PackageLoader, select_autoescape + + env = Environment( + loader=PackageLoader('freqtrade', 'templates'), + autoescape=select_autoescape(['html', 'xml']) + ) + template = env.get_template(templatefile) + + return template.render(**arguments) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index df1ff182c..05efa1164 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Optional, Dict from freqtrade import OperationalException -from freqtrade.constants import DEFAULT_HYPEROPT_LOSS +from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver @@ -58,7 +58,7 @@ class HyperOptResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='hyperopts', extra_dir=extra_dir) + user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, object_name=hyperopt_name, kwargs={'config': config}) @@ -110,7 +110,7 @@ class HyperOptLossResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='hyperopts', extra_dir=extra_dir) + user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, object_name=hyper_loss_name) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 102816981..9a76b9b74 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -129,7 +129,8 @@ class StrategyResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='strategies', extra_dir=extra_dir) + user_subdir=constants.USERPATH_STRATEGY, + extra_dir=extra_dir) if ":" in strategy_name: logger.info("loading base64 encoded strategy") diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 new file mode 100644 index 000000000..05ba08b81 --- /dev/null +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -0,0 +1,127 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +# --- Do not remove these libs --- +from functools import reduce +from typing import Any, Callable, Dict, List + +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real # noqa + +from freqtrade.optimize.hyperopt_interface import IHyperOpt + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class {{ hyperopt }}(IHyperOpt): + """ + This is a Hyperopt template to get you started. + + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md + + You should: + - Add any lib you need to build your hyperopt. + + You must keep: + - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. + + The roi_space, generate_roi_table, stoploss_space methods are no longer required to be + copied in every custom hyperopt. However, you may override them if you need the + 'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade. + Sample implementation of these methods can be found in + https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py + """ + + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + {{ buy_guards | indent(12) }} + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['close'], dataframe['sar'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend + + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + {{ buy_space | indent(12) }} + ] + + @staticmethod + def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the sell strategy parameters to be used by Hyperopt. + """ + def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Sell strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + {{ sell_guards | indent(12) }} + + # TRIGGERS + if 'sell-trigger' in params: + if params['sell-trigger'] == 'sell-bb_upper': + conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['sell-trigger'] == 'sell-macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macdsignal'], dataframe['macd'] + )) + if params['sell-trigger'] == 'sell-sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['sar'], dataframe['close'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 + + return dataframe + + return populate_sell_trend + + @staticmethod + def sell_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching sell strategy parameters. + """ + return [ + {{ sell_space | indent(12) }} + ] diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 new file mode 100644 index 000000000..73a4c7a5a --- /dev/null +++ b/freqtrade/templates/base_strategy.py.j2 @@ -0,0 +1,138 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +# --- Do not remove these libs --- +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame + +from freqtrade.strategy.interface import IStrategy + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class {{ strategy }}(IStrategy): + """ + This is a strategy template to get you started. + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + + You can: + :return: a Dataframe with all mandatory indicators for the strategies + - Rename the class name (Do not forget to update class_name) + - Add any methods you want to build your strategy + - Add any lib you need to build your strategy + + You must keep: + - the lib in the section "Do not remove these libs" + - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, + populate_sell_trend, hyperopt_space, buy_strategy_generator + """ + # Strategy interface version - allow new iterations of the strategy interface. + # Check the documentation or the Sample strategy to get the latest version. + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi". + minimal_roi = { + "60": 0.01, + "30": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy. + # This attribute will be overridden if the config file contains "stoploss". + stoploss = -0.10 + + # Trailing stoploss + trailing_stop = False + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured + + # Optimal ticker interval for the strategy. + ticker_interval = '5m' + + # Run "populate_indicators()" only for new candle. + process_only_new_candles = False + + # These values can be overridden in the "ask_strategy" section in the config. + use_sell_signal = True + sell_profit_only = False + ignore_roi_if_buy_signal = False + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional order type mapping. + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + # Optional order time in force. + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc' + } + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + {{ indicators | indent(8) }} + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + {{ buy_trend | indent(16) }} + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + {{ sell_trend | indent(16) }} + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'sell'] = 1 + return dataframe diff --git a/user_data/hyperopts/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py similarity index 97% rename from user_data/hyperopts/sample_hyperopt.py rename to freqtrade/templates/sample_hyperopt.py index 3be05f121..f1dcb404a 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -1,16 +1,21 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# --- Do not remove these libs --- from functools import reduce from typing import Any, Callable, Dict, List import numpy as np # noqa -import talib.abstract as ta +import pandas as pd # noqa from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib + class SampleHyperOpt(IHyperOpt): """ diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py similarity index 97% rename from user_data/hyperopts/sample_hyperopt_advanced.py rename to freqtrade/templates/sample_hyperopt_advanced.py index 66182edcf..5634c21ea 100644 --- a/user_data/hyperopts/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -1,18 +1,21 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# --- Do not remove these libs --- from functools import reduce -from math import exp from typing import Any, Callable, Dict, List -from datetime import datetime -import numpy as np# noqa F401 -import talib.abstract as ta +import numpy as np # noqa +import pandas as pd # noqa from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer, Real # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib + class AdvancedSampleHyperOpt(IHyperOpt): """ diff --git a/user_data/hyperopts/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py similarity index 100% rename from user_data/hyperopts/sample_hyperopt_loss.py rename to freqtrade/templates/sample_hyperopt_loss.py diff --git a/user_data/strategies/sample_strategy.py b/freqtrade/templates/sample_strategy.py similarity index 62% rename from user_data/strategies/sample_strategy.py rename to freqtrade/templates/sample_strategy.py index 77a2d261a..02bf24e7e 100644 --- a/user_data/strategies/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -1,13 +1,16 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # --- Do not remove these libs --- -from freqtrade.strategy.interface import IStrategy +import numpy as np # noqa +import pandas as pd # noqa from pandas import DataFrame -# -------------------------------- +from freqtrade.strategy.interface import IStrategy + +# -------------------------------- # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib -import numpy # noqa # This class is a sample. Feel free to customize it. @@ -110,19 +113,18 @@ class SampleStrategy(IStrategy): # ADX dataframe['adx'] = ta.ADX(dataframe) - """ - # Aroon, Aroon Oscillator - aroon = ta.AROON(dataframe) - dataframe['aroonup'] = aroon['aroonup'] - dataframe['aroondown'] = aroon['aroondown'] - dataframe['aroonosc'] = ta.AROONOSC(dataframe) + # # Aroon, Aroon Oscillator + # aroon = ta.AROON(dataframe) + # dataframe['aroonup'] = aroon['aroonup'] + # dataframe['aroondown'] = aroon['aroondown'] + # dataframe['aroonosc'] = ta.AROONOSC(dataframe) - # Awesome oscillator - dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + # # Awesome oscillator + # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + # dataframe['cci'] = ta.CCI(dataframe) - # Commodity Channel Index: values Oversold:<-100, Overbought:>100 - dataframe['cci'] = ta.CCI(dataframe) - """ # MACD macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] @@ -132,42 +134,39 @@ class SampleStrategy(IStrategy): # MFI dataframe['mfi'] = ta.MFI(dataframe) - """ - # Minus Directional Indicator / Movement - dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # # Minus Directional Indicator / Movement + # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # Plus Directional Indicator / Movement - dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # # Plus Directional Indicator / Movement + # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + # dataframe['plus_di'] = ta.PLUS_DI(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # ROC - dataframe['roc'] = ta.ROC(dataframe) + # # ROC + # dataframe['roc'] = ta.ROC(dataframe) - # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) - rsi = 0.1 * (dataframe['rsi'] - 50) - dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + # # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + # rsi = 0.1 * (dataframe['rsi'] - 50) + # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) - # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) - dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + # # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # # Stoch + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] - # Stoch - stoch = ta.STOCH(dataframe) - dataframe['slowd'] = stoch['slowd'] - dataframe['slowk'] = stoch['slowk'] - """ # Stoch fast stoch_fast = ta.STOCHF(dataframe) dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastk'] = stoch_fast['fastk'] - """ - # Stoch RSI - stoch_rsi = ta.STOCHRSI(dataframe) - dataframe['fastd_rsi'] = stoch_rsi['fastd'] - dataframe['fastk_rsi'] = stoch_rsi['fastk'] - """ + # # Stoch RSI + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] # Overlap Studies # ------------------------------------ @@ -178,17 +177,16 @@ class SampleStrategy(IStrategy): dataframe['bb_middleband'] = bollinger['mid'] dataframe['bb_upperband'] = bollinger['upper'] - """ - # EMA - Exponential Moving Average - dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + # # EMA - Exponential Moving Average + # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # # SMA - Simple Moving Average + # dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - # SMA - Simple Moving Average - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - """ # SAR Parabol dataframe['sar'] = ta.SAR(dataframe) @@ -204,65 +202,57 @@ class SampleStrategy(IStrategy): # Pattern Recognition - Bullish candlestick patterns # ------------------------------------ - """ - # Hammer: values [0, 100] - dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - # Inverted Hammer: values [0, 100] - dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - # Dragonfly Doji: values [0, 100] - dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - # Piercing Line: values [0, 100] - dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - # Morningstar: values [0, 100] - dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - # Three White Soldiers: values [0, 100] - dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - """ + # # Hammer: values [0, 100] + # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # # Inverted Hammer: values [0, 100] + # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # # Dragonfly Doji: values [0, 100] + # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # # Piercing Line: values [0, 100] + # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # # Morningstar: values [0, 100] + # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # # Three White Soldiers: values [0, 100] + # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] # Pattern Recognition - Bearish candlestick patterns # ------------------------------------ - """ - # Hanging Man: values [0, 100] - dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - # Shooting Star: values [0, 100] - dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - # Gravestone Doji: values [0, 100] - dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - # Dark Cloud Cover: values [0, 100] - dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - # Evening Doji Star: values [0, 100] - dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - # Evening Star: values [0, 100] - dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - """ + # # Hanging Man: values [0, 100] + # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # # Shooting Star: values [0, 100] + # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # # Gravestone Doji: values [0, 100] + # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # # Dark Cloud Cover: values [0, 100] + # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # # Evening Doji Star: values [0, 100] + # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # # Evening Star: values [0, 100] + # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) # Pattern Recognition - Bullish/Bearish candlestick patterns # ------------------------------------ - """ - # Three Line Strike: values [0, -100, 100] - dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - # Spinning Top: values [0, -100, 100] - dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - # Engulfing: values [0, -100, 100] - dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - # Harami: values [0, -100, 100] - dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - # Three Outside Up/Down: values [0, -100, 100] - dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - # Three Inside Up/Down: values [0, -100, 100] - dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - """ + # # Three Line Strike: values [0, -100, 100] + # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # # Spinning Top: values [0, -100, 100] + # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # # Engulfing: values [0, -100, 100] + # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # # Harami: values [0, -100, 100] + # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # # Three Outside Up/Down: values [0, -100, 100] + # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # # Three Inside Up/Down: values [0, -100, 100] + # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - # Chart type - # ------------------------------------ - """ - # Heikinashi stategy - heikinashi = qtpylib.heikinashi(dataframe) - dataframe['ha_open'] = heikinashi['open'] - dataframe['ha_close'] = heikinashi['close'] - dataframe['ha_high'] = heikinashi['high'] - dataframe['ha_low'] = heikinashi['low'] - """ + # # Chart type + # # ------------------------------------ + # # Heikinashi stategy + # heikinashi = qtpylib.heikinashi(dataframe) + # dataframe['ha_open'] = heikinashi['open'] + # dataframe['ha_close'] = heikinashi['close'] + # dataframe['ha_high'] = heikinashi['high'] + # dataframe['ha_low'] = heikinashi['low'] # Retrieve best bid and best ask from the orderbook # ------------------------------------ diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb similarity index 100% rename from user_data/notebooks/strategy_analysis_example.ipynb rename to freqtrade/templates/strategy_analysis_example.ipynb diff --git a/freqtrade/templates/subtemplates/buy_trend_full.j2 b/freqtrade/templates/subtemplates/buy_trend_full.j2 new file mode 100644 index 000000000..1a0d326b3 --- /dev/null +++ b/freqtrade/templates/subtemplates/buy_trend_full.j2 @@ -0,0 +1,3 @@ +(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 +(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle +(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising diff --git a/freqtrade/templates/subtemplates/buy_trend_minimal.j2 b/freqtrade/templates/subtemplates/buy_trend_minimal.j2 new file mode 100644 index 000000000..6a4079cf3 --- /dev/null +++ b/freqtrade/templates/subtemplates/buy_trend_minimal.j2 @@ -0,0 +1 @@ +(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 new file mode 100644 index 000000000..5b967f4ed --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 @@ -0,0 +1,8 @@ +if params.get('mfi-enabled'): + conditions.append(dataframe['mfi'] < params['mfi-value']) +if params.get('fastd-enabled'): + conditions.append(dataframe['fastd'] < params['fastd-value']) +if params.get('adx-enabled'): + conditions.append(dataframe['adx'] > params['adx-value']) +if params.get('rsi-enabled'): + conditions.append(dataframe['rsi'] < params['rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 new file mode 100644 index 000000000..5e1022f59 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 @@ -0,0 +1,2 @@ +if params.get('rsi-enabled'): + conditions.append(dataframe['rsi'] < params['rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 new file mode 100644 index 000000000..29bafbd93 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 @@ -0,0 +1,9 @@ +Integer(10, 25, name='mfi-value'), +Integer(15, 45, name='fastd-value'), +Integer(20, 50, name='adx-value'), +Integer(20, 40, name='rsi-value'), +Categorical([True, False], name='mfi-enabled'), +Categorical([True, False], name='fastd-enabled'), +Categorical([True, False], name='adx-enabled'), +Categorical([True, False], name='rsi-enabled'), +Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 new file mode 100644 index 000000000..5ddf537fb --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 @@ -0,0 +1,3 @@ +Integer(20, 40, name='rsi-value'), +Categorical([True, False], name='rsi-enabled'), +Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 new file mode 100644 index 000000000..bd7b499f4 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 @@ -0,0 +1,8 @@ +if params.get('sell-mfi-enabled'): + conditions.append(dataframe['mfi'] > params['sell-mfi-value']) +if params.get('sell-fastd-enabled'): + conditions.append(dataframe['fastd'] > params['sell-fastd-value']) +if params.get('sell-adx-enabled'): + conditions.append(dataframe['adx'] < params['sell-adx-value']) +if params.get('sell-rsi-enabled'): + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 new file mode 100644 index 000000000..8b4adebf6 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 @@ -0,0 +1,2 @@ +if params.get('sell-rsi-enabled'): + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 new file mode 100644 index 000000000..46469d532 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 @@ -0,0 +1,11 @@ +Integer(75, 100, name='sell-mfi-value'), +Integer(50, 100, name='sell-fastd-value'), +Integer(50, 100, name='sell-adx-value'), +Integer(60, 100, name='sell-rsi-value'), +Categorical([True, False], name='sell-mfi-enabled'), +Categorical([True, False], name='sell-fastd-enabled'), +Categorical([True, False], name='sell-adx-enabled'), +Categorical([True, False], name='sell-rsi-enabled'), +Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 new file mode 100644 index 000000000..dfb110543 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 @@ -0,0 +1,5 @@ +Integer(60, 100, name='sell-rsi-value'), +Categorical([True, False], name='sell-rsi-enabled'), +Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 new file mode 100644 index 000000000..879a2daa0 --- /dev/null +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -0,0 +1,161 @@ + +# Momentum Indicators +# ------------------------------------ + +# RSI +dataframe['rsi'] = ta.RSI(dataframe) + +# ADX +dataframe['adx'] = ta.ADX(dataframe) + +# # Aroon, Aroon Oscillator +# aroon = ta.AROON(dataframe) +# dataframe['aroonup'] = aroon['aroonup'] +# dataframe['aroondown'] = aroon['aroondown'] +# dataframe['aroonosc'] = ta.AROONOSC(dataframe) + +# # Awesome oscillator +# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + +# # Commodity Channel Index: values Oversold:<-100, Overbought:>100 +# dataframe['cci'] = ta.CCI(dataframe) + +# MACD +macd = ta.MACD(dataframe) +dataframe['macd'] = macd['macd'] +dataframe['macdsignal'] = macd['macdsignal'] +dataframe['macdhist'] = macd['macdhist'] + +# MFI +dataframe['mfi'] = ta.MFI(dataframe) + +# # Minus Directional Indicator / Movement +# dataframe['minus_dm'] = ta.MINUS_DM(dataframe) +# dataframe['minus_di'] = ta.MINUS_DI(dataframe) + +# # Plus Directional Indicator / Movement +# dataframe['plus_dm'] = ta.PLUS_DM(dataframe) +# dataframe['plus_di'] = ta.PLUS_DI(dataframe) +# dataframe['minus_di'] = ta.MINUS_DI(dataframe) + +# # ROC +# dataframe['roc'] = ta.ROC(dataframe) + +# # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) +# rsi = 0.1 * (dataframe['rsi'] - 50) +# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + +# # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) +# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + +# # Stoch +# stoch = ta.STOCH(dataframe) +# dataframe['slowd'] = stoch['slowd'] +# dataframe['slowk'] = stoch['slowk'] + +# Stoch fast +stoch_fast = ta.STOCHF(dataframe) +dataframe['fastd'] = stoch_fast['fastd'] +dataframe['fastk'] = stoch_fast['fastk'] + +# # Stoch RSI +# stoch_rsi = ta.STOCHRSI(dataframe) +# dataframe['fastd_rsi'] = stoch_rsi['fastd'] +# dataframe['fastk_rsi'] = stoch_rsi['fastk'] + +# Overlap Studies +# ------------------------------------ + +# Bollinger bands +bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) +dataframe['bb_lowerband'] = bollinger['lower'] +dataframe['bb_middleband'] = bollinger['mid'] +dataframe['bb_upperband'] = bollinger['upper'] + +# # EMA - Exponential Moving Average +# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) +# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) +# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) +# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) +# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + +# # SMA - Simple Moving Average +# dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + +# SAR Parabol +dataframe['sar'] = ta.SAR(dataframe) + +# TEMA - Triple Exponential Moving Average +dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + +# Cycle Indicator +# ------------------------------------ +# Hilbert Transform Indicator - SineWave +hilbert = ta.HT_SINE(dataframe) +dataframe['htsine'] = hilbert['sine'] +dataframe['htleadsine'] = hilbert['leadsine'] + +# Pattern Recognition - Bullish candlestick patterns +# ------------------------------------ +# # Hammer: values [0, 100] +# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) +# # Inverted Hammer: values [0, 100] +# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) +# # Dragonfly Doji: values [0, 100] +# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) +# # Piercing Line: values [0, 100] +# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] +# # Morningstar: values [0, 100] +# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] +# # Three White Soldiers: values [0, 100] +# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + +# Pattern Recognition - Bearish candlestick patterns +# ------------------------------------ +# # Hanging Man: values [0, 100] +# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) +# # Shooting Star: values [0, 100] +# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) +# # Gravestone Doji: values [0, 100] +# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) +# # Dark Cloud Cover: values [0, 100] +# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) +# # Evening Doji Star: values [0, 100] +# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) +# # Evening Star: values [0, 100] +# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + +# Pattern Recognition - Bullish/Bearish candlestick patterns +# ------------------------------------ +# # Three Line Strike: values [0, -100, 100] +# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) +# # Spinning Top: values [0, -100, 100] +# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] +# # Engulfing: values [0, -100, 100] +# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] +# # Harami: values [0, -100, 100] +# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] +# # Three Outside Up/Down: values [0, -100, 100] +# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] +# # Three Inside Up/Down: values [0, -100, 100] +# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + +# # Chart type +# # ------------------------------------ +# # Heikinashi stategy +# heikinashi = qtpylib.heikinashi(dataframe) +# dataframe['ha_open'] = heikinashi['open'] +# dataframe['ha_close'] = heikinashi['close'] +# dataframe['ha_high'] = heikinashi['high'] +# dataframe['ha_low'] = heikinashi['low'] + +# Retrieve best bid and best ask from the orderbook +# ------------------------------------ +""" +# first check if dataprovider is available +if self.dp: + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] +""" diff --git a/freqtrade/templates/subtemplates/indicators_minimal.j2 b/freqtrade/templates/subtemplates/indicators_minimal.j2 new file mode 100644 index 000000000..7d75b4610 --- /dev/null +++ b/freqtrade/templates/subtemplates/indicators_minimal.j2 @@ -0,0 +1,17 @@ + +# Momentum Indicators +# ------------------------------------ + +# RSI +dataframe['rsi'] = ta.RSI(dataframe) + +# Retrieve best bid and best ask from the orderbook +# ------------------------------------ +""" +# first check if dataprovider is available +if self.dp: + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] +""" diff --git a/freqtrade/templates/subtemplates/sell_trend_full.j2 b/freqtrade/templates/subtemplates/sell_trend_full.j2 new file mode 100644 index 000000000..36c08c947 --- /dev/null +++ b/freqtrade/templates/subtemplates/sell_trend_full.j2 @@ -0,0 +1,3 @@ +(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 +(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle +(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling diff --git a/freqtrade/templates/subtemplates/sell_trend_minimal.j2 b/freqtrade/templates/subtemplates/sell_trend_minimal.j2 new file mode 100644 index 000000000..42a7b81a2 --- /dev/null +++ b/freqtrade/templates/subtemplates/sell_trend_minimal.j2 @@ -0,0 +1 @@ +(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 0139006d6..99e8c3918 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,3 +1,4 @@ +import csv import logging import sys from collections import OrderedDict @@ -6,20 +7,22 @@ from pathlib import Path from typing import Any, Dict, List import arrow -import csv import rapidjson from colorama import init as colorama_init from tabulate import tabulate from freqtrade import OperationalException -from freqtrade.configuration import Configuration, TimeRange, remove_credentials -from freqtrade.configuration.directory_operations import create_userdata_dir +from freqtrade.configuration import (Configuration, TimeRange, + remove_credentials) +from freqtrade.configuration.directory_operations import (copy_sample_files, + create_userdata_dir) +from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) -from freqtrade.exchange import (available_exchanges, ccxt_exchanges, market_is_active, - symbol_is_pair) -from freqtrade.misc import plural +from freqtrade.exchange import (available_exchanges, ccxt_exchanges, + market_is_active, symbol_is_pair) +from freqtrade.misc import plural, render_template from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -84,12 +87,95 @@ def start_create_userdir(args: Dict[str, Any]) -> None: :return: None """ if "user_data_dir" in args and args["user_data_dir"]: - create_userdata_dir(args["user_data_dir"], create_dir=True) + userdir = create_userdata_dir(args["user_data_dir"], create_dir=True) + copy_sample_files(userdir, overwrite=args["reset"]) else: logger.warning("`create-userdir` requires --userdir to be set.") sys.exit(1) +def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str): + """ + Deploy new strategy from template to strategy_path + """ + indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",) + buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",) + sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",) + + strategy_text = render_template(templatefile='base_strategy.py.j2', + arguments={"strategy": strategy_name, + "indicators": indicators, + "buy_trend": buy_trend, + "sell_trend": sell_trend, + }) + + logger.info(f"Writing strategy to `{strategy_path}`.") + strategy_path.write_text(strategy_text) + + +def start_new_strategy(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + if "strategy" in args and args["strategy"]: + if args["strategy"] == "DefaultStrategy": + raise OperationalException("DefaultStrategy is not allowed as name.") + + new_path = config['user_data_dir'] / USERPATH_STRATEGY / (args["strategy"] + ".py") + + if new_path.exists(): + raise OperationalException(f"`{new_path}` already exists. " + "Please choose another Strategy Name.") + + deploy_new_strategy(args['strategy'], new_path, args['template']) + + else: + raise OperationalException("`new-strategy` requires --strategy to be set.") + + +def deploy_new_hyperopt(hyperopt_name, hyperopt_path: Path, subtemplate: str): + """ + Deploys a new hyperopt template to hyperopt_path + """ + buy_guards = render_template( + templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",) + sell_guards = render_template( + templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",) + buy_space = render_template( + templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",) + sell_space = render_template( + templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",) + + strategy_text = render_template(templatefile='base_hyperopt.py.j2', + arguments={"hyperopt": hyperopt_name, + "buy_guards": buy_guards, + "sell_guards": sell_guards, + "buy_space": buy_space, + "sell_space": sell_space, + }) + + logger.info(f"Writing hyperopt to `{hyperopt_path}`.") + hyperopt_path.write_text(strategy_text) + + +def start_new_hyperopt(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + if "hyperopt" in args and args["hyperopt"]: + if args["hyperopt"] == "DefaultHyperopt": + raise OperationalException("DefaultHyperopt is not allowed as name.") + + new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args["hyperopt"] + ".py") + + if new_path.exists(): + raise OperationalException(f"`{new_path}` already exists. " + "Please choose another Strategy Name.") + deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) + else: + raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") + + def start_download_data(args: Dict[str, Any]) -> None: """ Download data (former download_backtest_data.py script) diff --git a/requirements-common.txt b/requirements-common.txt index 63cf48eee..e5c66590a 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.19.54 +ccxt==1.19.86 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 @@ -8,10 +8,11 @@ cachetools==3.1.1 requests==2.22.0 urllib3==1.25.7 wrapt==1.11.2 -jsonschema==3.1.1 +jsonschema==3.2.0 TA-Lib==0.4.17 tabulate==0.8.6 coinmarketcap==5.0.3 +jinja2==2.10.3 # find first, C search in arrays py_find_1st==1.1.4 diff --git a/requirements-dev.txt b/requirements-dev.txt index 297b95623..a60bbf0eb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,10 +8,10 @@ flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.1.0 mypy==0.740 -pytest==5.2.4 +pytest==5.3.0 pytest-asyncio==0.10.0 pytest-cov==2.8.1 -pytest-mock==1.11.2 +pytest-mock==1.12.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index ff8de9cb2..96a22b42e 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.3.2 +scipy==1.3.3 scikit-learn==0.21.3 scikit-optimize==0.5.2 filelock==3.0.12 diff --git a/setup.py b/setup.py index 50b8eee9c..3710bcdc0 100644 --- a/setup.py +++ b/setup.py @@ -78,6 +78,7 @@ setup(name='freqtrade', 'python-rapidjson', 'sdnotify', 'colorama', + 'jinja2', # from requirements.txt 'numpy', 'pandas', diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index b722dd3f8..e74ead33d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -869,6 +869,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'backtesting', '--config', 'config.json', '--datadir', str(testdatadir), + '--strategy-path', str(Path(__file__).parents[2] / 'freqtrade/templates'), '--ticker-interval', '1m', '--timerange', '1510694220-1510700340', '--enable-position-stacking', diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 97affc99c..963d36c76 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -36,13 +36,15 @@ def test_search_strategy(): def test_load_strategy(default_conf, result): - default_conf.update({'strategy': 'SampleStrategy'}) + default_conf.update({'strategy': 'SampleStrategy', + 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') + }) resolver = StrategyResolver(default_conf) assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_base64(result, caplog, default_conf): - with open("user_data/strategies/sample_strategy.py", "rb") as file: + with (Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py').open("rb") as file: encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) @@ -54,10 +56,10 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): - default_conf['strategy'] = 'SampleStrategy' + default_conf['strategy'] = 'DefaultStrategy' resolver = StrategyResolver(default_conf) extra_dir = Path.cwd() / 'some/path' - resolver._load_strategy('SampleStrategy', config=default_conf, extra_dir=extra_dir) + resolver._load_strategy('DefaultStrategy', config=default_conf, extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 240b7c784..e971d15ab 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -17,8 +17,6 @@ from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import ( check_conflicting_settings, process_deprecated_setting, process_temporary_deprecated_settings) -from freqtrade.configuration.directory_operations import (create_datadir, - create_userdata_dir) from freqtrade.configuration.load_config import load_config_file from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers @@ -670,45 +668,6 @@ def test_validate_default_conf(default_conf) -> None: validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) -def test_create_datadir(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - create_datadir(default_conf, '/foo/bar') - assert md.call_args[1]['parents'] is True - assert log_has('Created data directory: /foo/bar', caplog) - - -def test_create_userdata_dir(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - x = create_userdata_dir('/tmp/bar', create_dir=True) - assert md.call_count == 7 - assert md.call_args[1]['parents'] is False - assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog) - assert isinstance(x, Path) - assert str(x) == str(Path("/tmp/bar")) - - -def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - create_userdata_dir('/tmp/bar') - assert md.call_count == 0 - - -def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - with pytest.raises(OperationalException, - match=r'Directory `.{1,2}tmp.{1,2}bar` does not exist.*'): - create_userdata_dir('/tmp/bar', create_dir=False) - assert md.call_count == 0 - - def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 with pytest.raises(OperationalException, match='The config stoploss needs to be different ' diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py new file mode 100644 index 000000000..db41e2da2 --- /dev/null +++ b/tests/test_directory_operations.py @@ -0,0 +1,91 @@ +# pragma pylint: disable=missing-docstring, protected-access, invalid-name +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from freqtrade import OperationalException +from freqtrade.configuration.directory_operations import (copy_sample_files, + create_datadir, + create_userdata_dir) +from tests.conftest import log_has, log_has_re + + +def test_create_datadir(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + create_datadir(default_conf, '/foo/bar') + assert md.call_args[1]['parents'] is True + assert log_has('Created data directory: /foo/bar', caplog) + + +def test_create_userdata_dir(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + x = create_userdata_dir('/tmp/bar', create_dir=True) + assert md.call_count == 8 + assert md.call_args[1]['parents'] is False + assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog) + assert isinstance(x, Path) + assert str(x) == str(Path("/tmp/bar")) + + +def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + create_userdata_dir('/tmp/bar') + assert md.call_count == 0 + + +def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + with pytest.raises(OperationalException, + match=r'Directory `.{1,2}tmp.{1,2}bar` does not exist.*'): + create_userdata_dir('/tmp/bar', create_dir=False) + assert md.call_count == 0 + + +def test_copy_sample_files(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + copymock = mocker.patch('shutil.copy', MagicMock()) + + copy_sample_files(Path('/tmp/bar')) + assert copymock.call_count == 5 + assert copymock.call_args_list[0][0][1] == str( + Path('/tmp/bar') / 'strategies/sample_strategy.py') + assert copymock.call_args_list[1][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt_advanced.py') + assert copymock.call_args_list[2][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt_loss.py') + assert copymock.call_args_list[3][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt.py') + assert copymock.call_args_list[4][0][1] == str( + Path('/tmp/bar') / 'notebooks/strategy_analysis_example.ipynb') + + +def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + mocker.patch('shutil.copy', MagicMock()) + with pytest.raises(OperationalException, + match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist\."): + copy_sample_files(Path('/tmp/bar')) + + mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False])) + + with pytest.raises(OperationalException, + match=r"Directory `.{1,2}tmp.{1,2}bar.{1,2}strategies` does not exist\."): + copy_sample_files(Path('/tmp/bar')) + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + copy_sample_files(Path('/tmp/bar')) + assert log_has_re(r"File `.*` exists already, not deploying sample file\.", caplog) + caplog.clear() + copy_sample_files(Path('/tmp/bar'), overwrite=True) + assert log_has_re(r"File `.*` exists already, overwriting\.", caplog) diff --git a/tests/test_utils.py b/tests/test_utils.py index bbb4fc648..1258c939c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,8 +9,9 @@ from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, start_download_data, start_list_exchanges, start_list_markets, start_list_timeframes, + start_new_hyperopt, start_new_strategy, start_trading) -from tests.conftest import get_args, log_has, patch_exchange +from tests.conftest import get_args, log_has, log_has_re, patch_exchange def test_setup_utils_configuration(): @@ -442,6 +443,7 @@ def test_create_datadir_failed(caplog): def test_create_datadir(caplog, mocker): cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) + csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock()) args = [ "create-userdir", "--userdir", @@ -450,9 +452,82 @@ def test_create_datadir(caplog, mocker): start_create_userdir(get_args(args)) assert cud.call_count == 1 + assert csf.call_count == 1 assert len(caplog.record_tuples) == 0 +def test_start_new_strategy(mocker, caplog): + wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + + args = [ + "new-strategy", + "--strategy", + "CoolNewStrategy" + ] + start_new_strategy(get_args(args)) + + assert wt_mock.call_count == 1 + assert "CoolNewStrategy" in wt_mock.call_args_list[0][0][0] + assert log_has_re("Writing strategy to .*", caplog) + + +def test_start_new_strategy_DefaultStrat(mocker, caplog): + args = [ + "new-strategy", + "--strategy", + "DefaultStrategy" + ] + with pytest.raises(OperationalException, + match=r"DefaultStrategy is not allowed as name\."): + start_new_strategy(get_args(args)) + + +def test_start_new_strategy_no_arg(mocker, caplog): + args = [ + "new-strategy", + ] + with pytest.raises(OperationalException, + match="`new-strategy` requires --strategy to be set."): + start_new_strategy(get_args(args)) + + +def test_start_new_hyperopt(mocker, caplog): + wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + + args = [ + "new-hyperopt", + "--hyperopt", + "CoolNewhyperopt" + ] + start_new_hyperopt(get_args(args)) + + assert wt_mock.call_count == 1 + assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0] + assert log_has_re("Writing hyperopt to .*", caplog) + + +def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): + args = [ + "new-hyperopt", + "--hyperopt", + "DefaultHyperopt" + ] + with pytest.raises(OperationalException, + match=r"DefaultHyperopt is not allowed as name\."): + start_new_hyperopt(get_args(args)) + + +def test_start_new_hyperopt_no_arg(mocker, caplog): + args = [ + "new-hyperopt", + ] + with pytest.raises(OperationalException, + match="`new-hyperopt` requires --hyperopt to be set."): + start_new_hyperopt(get_args(args)) + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) diff --git a/user_data/hyperopts/__init__.py b/user_data/hyperopts/.gitkeep similarity index 100% rename from user_data/hyperopts/__init__.py rename to user_data/hyperopts/.gitkeep diff --git a/user_data/strategies/__init__.py b/user_data/strategies/.gitkeep similarity index 100% rename from user_data/strategies/__init__.py rename to user_data/strategies/.gitkeep