From c85cd13ca1a5bdbba07659d989a3f09de9464456 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 13:31:42 +0200 Subject: [PATCH 001/269] Change default backtest result to "backtest_results" - backtest_data is misleading --- docs/backtesting.md | 2 +- docs/bot-usage.md | 4 ++-- docs/data-analysis.md | 2 +- docs/plotting.md | 2 +- freqtrade/configuration/cli_options.py | 4 ++-- user_data/backtest_results/.gitkeep | 0 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 user_data/backtest_results/.gitkeep diff --git a/docs/backtesting.md b/docs/backtesting.md index 179bcee15..57f9f6296 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -237,7 +237,7 @@ All listed Strategies need to be in the same directory. freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades ``` -This will save the results to `user_data/backtest_data/backtest-result-.json`, injecting the strategy-name into the target filename. +This will save the results to `user_data/backtest_results/backtest-result-.json`, injecting the strategy-name into the target filename. There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table). Detailed output for all strategies one after the other will be available, so make sure to scroll up. diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8877ed010..ef28eb60c 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -176,8 +176,8 @@ optional arguments: --export-filename PATH Save backtest results to this filename requires --export to be set as well Example --export- - filename=user_data/backtest_data/backtest_today.json - (default: user_data/backtest_data/backtest- + filename=user_data/backtest_results/backtest_today.json + (default: user_data/backtest_results/backtest- result.json) ``` diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 1940fa3e6..cda7a9a57 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -15,7 +15,7 @@ Freqtrade provides the `load_backtest_data()` helper function to easily load the ``` python from freqtrade.data.btanalysis import load_backtest_data -df = load_backtest_data("user_data/backtest-result.json") +df = load_backtest_data("user_data/backtest_results/backtest-result.json") # Show value-counts per pair df.groupby("pair")["sell_reason"].value_counts() diff --git a/docs/plotting.md b/docs/plotting.md index b8e041d61..acab60b23 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -64,7 +64,7 @@ python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p To plot trades from a backtesting result, use `--export-filename ` ``` bash -python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH +python3 scripts/plot_dataframe.py --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH ``` To plot a custom strategy the strategy should have first be backtested. diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 04fde2051..4ec280d0f 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -146,9 +146,9 @@ AVAILABLE_CLI_OPTIONS = { '--export-filename', help='Save backtest results to the file with this filename (default: `%(default)s`). ' 'Requires `--export` to be set as well. ' - 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', + 'Example: `--export-filename=user_data/backtest_results/backtest_today.json`', metavar='PATH', - default=os.path.join('user_data', 'backtest_data', + default=os.path.join('user_data', 'backtest_results', 'backtest-result.json'), ), # Edge diff --git a/user_data/backtest_results/.gitkeep b/user_data/backtest_results/.gitkeep new file mode 100644 index 000000000..e69de29bb From 6c3a0eb1d6a29df271bf0a32fa8baddec591acd4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 13:42:56 +0200 Subject: [PATCH 002/269] add create_userdir function --- freqtrade/configuration/configuration.py | 2 +- ...create_datadir.py => folder_operations.py} | 14 +++++++++++++ freqtrade/tests/test_configuration.py | 20 ++++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) rename freqtrade/configuration/{create_datadir.py => folder_operations.py} (56%) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 17ad37d6a..d96a291e2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -10,7 +10,7 @@ from typing import Any, Callable, Dict, Optional from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.create_datadir import create_datadir +from freqtrade.configuration.folder_operations import create_datadir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts diff --git a/freqtrade/configuration/create_datadir.py b/freqtrade/configuration/folder_operations.py similarity index 56% rename from freqtrade/configuration/create_datadir.py rename to freqtrade/configuration/folder_operations.py index acc3a29ca..f402f91e0 100644 --- a/freqtrade/configuration/create_datadir.py +++ b/freqtrade/configuration/folder_operations.py @@ -18,3 +18,17 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str folder.mkdir(parents=True) logger.info(f'Created data directory: {datadir}') return str(folder) + + +def create_userdata_dir(directory: str): + sub_dirs = ["backtest_results", "data", "hyperopts", "plots", "strategies", ] + folder = Path(directory) + if not folder.is_dir(): + folder.mkdir(parents=True) + logger.info(f'Created user-data directory: {folder}') + + # Create required subdirectories + for f in sub_dirs: + subfolder = folder / f + if not subfolder.is_dir(): + subfolder.mkdir(parents=False) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 56ff79625..21df8d4e5 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -13,7 +13,7 @@ from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants from freqtrade.configuration import Arguments, Configuration from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.create_datadir import create_datadir +from freqtrade.configuration.folder_operations import create_datadir, create_userdata_dir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers @@ -609,6 +609,24 @@ def test_create_datadir(mocker, default_conf, caplog) -> None: assert log_has('Created data directory: /foo/bar', caplog.record_tuples) +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()) + + create_userdata_dir('/tmp/bar') + assert md.call_count == 6 + assert md.call_args[1]['parents'] is False + assert log_has('Created user-data directory: /tmp/bar', caplog.record_tuples) + + +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_validate_tsl(default_conf): default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0 From 23435512c461a0b3331314f4e06911d90fe94497 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 14:13:38 +0200 Subject: [PATCH 003/269] Add create-userdir command to initialize a user directory --- freqtrade/configuration/arguments.py | 10 ++++++++-- freqtrade/configuration/cli_options.py | 5 +++++ freqtrade/configuration/configuration.py | 9 ++++++++- freqtrade/tests/conftest.py | 1 + freqtrade/utils.py | 16 +++++++++++++++- 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 4f0c3d31b..0651d9544 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -31,6 +31,8 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_LIST_EXCHANGES = ["print_one_column"] +ARGS_CREATE_USERDIR = ["user_data_dir"] + ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + @@ -106,8 +108,7 @@ class Arguments(object): :return: None """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge - from freqtrade.utils import start_list_exchanges - + from freqtrade.utils import start_create_userdir, start_list_exchanges subparsers = self.parser.add_subparsers(dest='subparser') # Add backtesting subcommand @@ -125,6 +126,11 @@ class Arguments(object): hyperopt_cmd.set_defaults(func=start_hyperopt) self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) + create_userdir_cmd = subparsers.add_parser('create-userdir', + help="Create user-data directory.") + create_userdir_cmd.set_defaults(func=start_create_userdir) + self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_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 4ec280d0f..00c28ed07 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -58,6 +58,11 @@ AVAILABLE_CLI_OPTIONS = { help='Path to backtest data.', metavar='PATH', ), + "user_data_dir": Arg( + '--userdir', '--user-data-dir', + help='Path to Userdata Directory.', + metavar='PATH', + ), # Main options "strategy": Arg( '-s', '--strategy', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index d96a291e2..0fd092728 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -6,11 +6,12 @@ import logging import sys import warnings from argparse import Namespace +from pathlib import Path from typing import Any, Callable, Dict, Optional from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.folder_operations import create_datadir +from freqtrade.configuration.folder_operations import create_datadir, create_userdata_dir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts @@ -173,6 +174,12 @@ class Configuration(object): Extract information for sys.argv and load datadir configuration: the --datadir option """ + if 'user_data_dir' in self.args and self.args.user_data_dir: + config.update({'user_data_dir': self.args.user_data_dir}) + create_userdata_dir(config['user_data_dir']) + elif 'user_data_dir' not in config: + config.update({'user_data_dir': str(Path.cwd() / "user_data")}) + if 'datadir' in self.args and self.args.datadir: config.update({'datadir': create_datadir(config, self.args.datadir)}) else: diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 5862a2e89..de3b2e4d9 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -234,6 +234,7 @@ def default_conf(): }, "initial_state": "running", "db_url": "sqlite://", + "user_data_dir": "user_data", "verbosity": 3, } return configuration diff --git a/freqtrade/utils.py b/freqtrade/utils.py index d550ef43c..53325b70b 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,12 +1,13 @@ import logging +import sys from argparse import Namespace from typing import Any, Dict from freqtrade.configuration import Configuration +from freqtrade.configuration.folder_operations import create_userdata_dir from freqtrade.exchange import available_exchanges from freqtrade.state import RunMode - logger = logging.getLogger(__name__) @@ -39,3 +40,16 @@ def start_list_exchanges(args: Namespace) -> None: else: print(f"Exchanges supported by ccxt and available for Freqtrade: " f"{', '.join(available_exchanges())}") + + +def start_create_userdir(args: Namespace) -> None: + """ + Create "user_data" directory to contain user data strategies, hyperopts, ...) + :param args: Cli args from Arguments() + :return: None + """ + if "user_data_dir" in args and args.user_data_dir: + create_userdata_dir(args.user_data_dir) + else: + logger.warning("`create-userdir` requires --userdir to be set.") + sys.exit(1) From 56c8bdbaa24b72d64d260a2dc1dcee030f65f478 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 14:32:00 +0200 Subject: [PATCH 004/269] Test create-userdir command line option --- freqtrade/tests/test_utils.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index a12b709d7..2bbece33f 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -1,8 +1,12 @@ -from freqtrade.utils import setup_utils_configuration, start_list_exchanges -from freqtrade.tests.conftest import get_args -from freqtrade.state import RunMode - import re +from unittest.mock import MagicMock + +import pytest + +from freqtrade.state import RunMode +from freqtrade.tests.conftest import get_args, log_has +from freqtrade.utils import (setup_utils_configuration, start_create_userdir, + start_list_exchanges) def test_setup_utils_configuration(): @@ -40,3 +44,26 @@ def test_list_exchanges(capsys): assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out) assert re.search(r"^binance$", captured.out, re.MULTILINE) assert re.search(r"^bittrex$", captured.out, re.MULTILINE) + + +def test_create_datadir_failed(caplog): + + args = [ + "create-userdir", + ] + with pytest.raises(SystemExit): + start_create_userdir(get_args(args)) + assert log_has("`create-userdir` requires --userdir to be set.", caplog.record_tuples) + + +def test_create_datadir(caplog, mocker): + cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) + args = [ + "create-userdir", + "--userdir", + "/temp/freqtrade/test" + ] + start_create_userdir(get_args(args)) + + assert cud.call_count == 1 + assert len(caplog.record_tuples) == 0 From 1b2581f0cb7c4ed9f15619533592fa9a9aad915d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 14:32:29 +0200 Subject: [PATCH 005/269] Add user_data_dir to configuration --- freqtrade/configuration/arguments.py | 2 +- freqtrade/configuration/configuration.py | 6 +++++- freqtrade/configuration/folder_operations.py | 6 ++++-- freqtrade/tests/test_configuration.py | 16 +++++++++++++--- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 0651d9544..ef19d121e 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -9,7 +9,7 @@ import arrow from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade import constants -ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] +ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] ARGS_STRATEGY = ["strategy", "strategy_path"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 0fd092728..78584ce6a 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -176,9 +176,13 @@ class Configuration(object): """ if 'user_data_dir' in self.args and self.args.user_data_dir: config.update({'user_data_dir': self.args.user_data_dir}) - create_userdata_dir(config['user_data_dir']) elif 'user_data_dir' not in config: + # Default to cwd/user_data (legacy option ...) config.update({'user_data_dir': str(Path.cwd() / "user_data")}) + # reset to user_data_dir so this contains the absolute path. + config['user_data_dir'] = create_userdata_dir(config['user_data_dir']) + + logger.info('Using user-data directory: %s ...', config['user_data_dir']) if 'datadir' in self.args and self.args.datadir: config.update({'datadir': create_datadir(config, self.args.datadir)}) diff --git a/freqtrade/configuration/folder_operations.py b/freqtrade/configuration/folder_operations.py index f402f91e0..d43dd1bf1 100644 --- a/freqtrade/configuration/folder_operations.py +++ b/freqtrade/configuration/folder_operations.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str: - folder = Path(datadir) if datadir else Path('user_data/data') + folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data") if not datadir: # set datadir exchange_name = config.get('exchange', {}).get('name').lower() @@ -20,7 +20,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str return str(folder) -def create_userdata_dir(directory: str): +def create_userdata_dir(directory: str) -> str: sub_dirs = ["backtest_results", "data", "hyperopts", "plots", "strategies", ] folder = Path(directory) if not folder.is_dir(): @@ -32,3 +32,5 @@ def create_userdata_dir(directory: str): subfolder = folder / f if not subfolder.is_dir(): subfolder.mkdir(parents=False) + # TODO: convert this to return Path + return str(folder.resolve()) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 21df8d4e5..4d1acc8d2 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -331,11 +331,15 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x ) - + mocker.patch( + 'freqtrade.configuration.configuration.create_userdata_dir', + lambda x: x + ) arglist = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', + '--userdir', "/tmp/freqtrade", 'backtesting', '--ticker-interval', '1m', '--live', @@ -357,7 +361,11 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format("/foo/bar"), + caplog.record_tuples + ) + assert log_has( + 'Using user-data directory: {} ...'.format("/tmp/freqtrade"), caplog.record_tuples ) assert 'ticker_interval' in config @@ -613,10 +621,12 @@ 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()) - create_userdata_dir('/tmp/bar') + x = create_userdata_dir('/tmp/bar') assert md.call_count == 6 assert md.call_args[1]['parents'] is False assert log_has('Created user-data directory: /tmp/bar', caplog.record_tuples) + assert isinstance(x, str) + assert x == "/tmp/bar" def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: From da755d1c83389a9d242923f480fcb23ae8f05a57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:37:52 +0200 Subject: [PATCH 006/269] Remove obsolete variable --- scripts/download_backtest_data.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index ed96cec71..e721d77ba 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -20,8 +20,6 @@ import logging logger = logging.getLogger('download_backtest_data') -DEFAULT_DL_PATH = 'user_data/data' - # Do not read the default config if config is not specified # in the command line options explicitely arguments = Arguments(sys.argv[1:], 'Download backtest data', From eab82fdec7ea73fa53e8a7f348e38a0414537046 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:49:52 +0200 Subject: [PATCH 007/269] plot-scripts use user_data_dir --- freqtrade/configuration/folder_operations.py | 2 +- freqtrade/plot/plotting.py | 6 +++--- freqtrade/tests/test_plotting.py | 4 +++- scripts/plot_dataframe.py | 3 ++- scripts/plot_profit.py | 3 ++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration/folder_operations.py b/freqtrade/configuration/folder_operations.py index d43dd1bf1..2ea9cd268 100644 --- a/freqtrade/configuration/folder_operations.py +++ b/freqtrade/configuration/folder_operations.py @@ -33,4 +33,4 @@ def create_userdata_dir(directory: str) -> str: if not subfolder.is_dir(): subfolder.mkdir(parents=False) # TODO: convert this to return Path - return str(folder.resolve()) + return folder diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index dde6f78f0..948964462 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -308,7 +308,7 @@ def generate_plot_filename(pair, ticker_interval) -> str: return file_name -def store_plot_file(fig, filename: str, auto_open: bool = False) -> None: +def store_plot_file(fig, filename: str, folder: Path, auto_open: bool = False) -> None: """ Generate a plot html file from pre populated fig plotly object :param fig: Plotly Figure to plot @@ -317,7 +317,7 @@ def store_plot_file(fig, filename: str, auto_open: bool = False) -> None: :return: None """ - Path("user_data/plots").mkdir(parents=True, exist_ok=True) + folder.mkdir(parents=True, exist_ok=True) - plot(fig, filename=str(Path('user_data/plots').joinpath(filename)), + plot(fig, filename=str(folder.joinpath(filename)), auto_open=auto_open) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 0ebadf720..c80d0e780 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -1,5 +1,6 @@ from copy import deepcopy +from pathlib import Path from unittest.mock import MagicMock import plotly.graph_objs as go @@ -209,7 +210,8 @@ def test_generate_Plot_filename(): def test_generate_plot_file(mocker, caplog): fig = generage_empty_figure() plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) - store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html") + store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html", + folder=Path("user_data/plots")) assert plot_mock.call_count == 1 assert plot_mock.call_args[0][0] == fig diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 034a6f448..04911f93e 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -77,7 +77,8 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): indicators2=config["indicators2"].split(",") ) - store_plot_file(fig, generate_plot_filename(pair, config['ticker_interval'])) + store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']), + folder=config['user_data_dir'] / "plot") logger.info('End of ploting process %s plots generated', pair_counter) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 4290bca45..c83ad1088 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -32,7 +32,8 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades) - store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) + store_plot_file(fig, filename='freqtrade-profit-plot.html', + folder=config['user_data_dir'] / "plot", auto_open=True) def plot_parse_args(args: List[str]) -> Dict[str, Any]: From ae0e00118736de14cb5337174d5865c5986e4c6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:56:32 +0200 Subject: [PATCH 008/269] Fix some bugs in tests --- freqtrade/tests/test_configuration.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 4d1acc8d2..15a2a3271 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -305,6 +305,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config + assert 'user_data_dir' in config assert log_has( 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples @@ -333,7 +334,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non ) mocker.patch( 'freqtrade.configuration.configuration.create_userdata_dir', - lambda x: x + lambda x: Path(x) ) arglist = [ '--config', 'config.json', @@ -368,6 +369,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non 'Using user-data directory: {} ...'.format("/tmp/freqtrade"), caplog.record_tuples ) + assert 'user_data_dir' in config + assert 'ticker_interval' in config assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', caplog.record_tuples) @@ -625,8 +628,8 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None: assert md.call_count == 6 assert md.call_args[1]['parents'] is False assert log_has('Created user-data directory: /tmp/bar', caplog.record_tuples) - assert isinstance(x, str) - assert x == "/tmp/bar" + assert isinstance(x, Path) + assert str(x) == "/tmp/bar" def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: From 0a253d66d07c4bb4090a981a6a9a57756060c4eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:56:44 +0200 Subject: [PATCH 009/269] Remove os.path from hyperopt --- freqtrade/optimize/hyperopt.py | 11 +++++------ freqtrade/tests/optimize/test_hyperopt.py | 14 +++++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 759ceffbe..a1ec43c14 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -5,7 +5,6 @@ This module contains the hyperopt logic """ import logging -import os import sys from operator import itemgetter @@ -31,9 +30,9 @@ logger = logging.getLogger(__name__) INITIAL_POINTS = 30 MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization -TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl') -TRIALSDATA_PICKLE = os.path.join('user_data', 'hyperopt_results.pickle') -HYPEROPT_LOCKFILE = os.path.join('user_data', 'hyperopt.lock') +TICKERDATA_PICKLE = Path.cwd() / 'user_data' / 'hyperopt_tickerdata.pkl' +TRIALSDATA_PICKLE = Path.cwd() / 'user_data' / 'hyperopt_results.pickle' +HYPEROPT_LOCKFILE = Path.cwd() / 'user_data' / 'hyperopt.lock' class Hyperopt(Backtesting): @@ -115,7 +114,7 @@ class Hyperopt(Backtesting): """ logger.info('Reading Trials from \'%s\'', self.trials_file) trials = load(self.trials_file) - os.remove(self.trials_file) + self.trials_file.unlink() return trials def log_trials_result(self) -> None: @@ -269,7 +268,7 @@ class Hyperopt(Backtesting): def load_previous_results(self): """ read trials file if we have one """ - if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0: + if self.trials_file.is_file() and self.trials_file.stat().st_size > 0: self.trials = self.read_trials() logger.info( 'Loaded %d previous evaluations from disk.', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index fad89e877..2370e145e 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,12 +1,13 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 import os from datetime import datetime -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import pandas as pd import pytest from arrow import Arrow from filelock import Timeout +from pathlib import Path from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe @@ -53,11 +54,14 @@ def create_trials(mocker, hyperopt) -> None: - we might have a pickle'd file so make sure that we return false when looking for it """ - hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') + hyperopt.trials_file = Path('freqtrade/tests/optimize/ut_trials.pickle') - mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False) - mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1) - mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True) + mocker.patch.object(Path, "is_file", MagicMock(return_value=False)) + stat_mock = MagicMock() + stat_mock.st_size = PropertyMock(return_value=1) + mocker.patch.object(Path, "stat", MagicMock(return_value=False)) + + mocker.patch.object(Path, "unlink", MagicMock(return_value=True)) mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) return [{'loss': 1, 'result': 'foo', 'params': {}}] From 113947132c0a6740ef6e8b19a37759a0b0715a57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 16:06:44 +0200 Subject: [PATCH 010/269] user_data_dir is PATH in config, not str --- freqtrade/tests/conftest.py | 2 +- freqtrade/tests/test_configuration.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index de3b2e4d9..0aaf28d96 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -234,7 +234,7 @@ def default_conf(): }, "initial_state": "running", "db_url": "sqlite://", - "user_data_dir": "user_data", + "user_data_dir": Path("user_data"), "verbosity": 3, } return configuration diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 15a2a3271..3d75818d4 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -53,6 +53,7 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None: + del default_conf['user_data_dir'] file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) From 2c7a248307356fb6170ac2aa79c7f9e5443e0066 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 16:07:06 +0200 Subject: [PATCH 011/269] Use user_data_dir in hyperopt --- freqtrade/optimize/__init__.py | 4 ++-- freqtrade/optimize/hyperopt.py | 17 ++++++++++------- freqtrade/tests/optimize/test_hyperopt.py | 9 ++++----- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 8b548eefe..2c7c42c4d 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -64,14 +64,14 @@ def start_hyperopt(args: Namespace) -> None: :return: None """ # Import here to avoid loading hyperopt module when it's not used - from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE + from freqtrade.optimize.hyperopt import Hyperopt # Initialize configuration config = setup_configuration(args, RunMode.HYPEROPT) logger.info('Starting freqtrade in Hyperopt mode') - lock = FileLock(HYPEROPT_LOCKFILE) + lock = FileLock(Hyperopt.get_lock_filename(config)) try: with lock.acquire(timeout=1): diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a1ec43c14..2fff91e58 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -30,9 +30,6 @@ logger = logging.getLogger(__name__) INITIAL_POINTS = 30 MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization -TICKERDATA_PICKLE = Path.cwd() / 'user_data' / 'hyperopt_tickerdata.pkl' -TRIALSDATA_PICKLE = Path.cwd() / 'user_data' / 'hyperopt_results.pickle' -HYPEROPT_LOCKFILE = Path.cwd() / 'user_data' / 'hyperopt.lock' class Hyperopt(Backtesting): @@ -50,6 +47,8 @@ class Hyperopt(Backtesting): self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function + self.trials_file = self.config['user_data_dir'] / 'hyperopt_results.pickle' + self.tickerdata_pickle = self.config['user_data_dir'] / 'hyperopt_tickerdata.pkl' self.total_tries = config.get('epochs', 0) self.current_best_loss = 100 @@ -59,7 +58,6 @@ class Hyperopt(Backtesting): logger.info("Continuing on previous hyperopt results.") # Previous evaluations - self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] # Populate functions here (hasattr is slow so should not be run during "regular" operations) @@ -77,11 +75,16 @@ class Hyperopt(Backtesting): self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False), + @staticmethod + def get_lock_filename(config) -> str: + + return str(config['user_data_dir'] / 'hyperopt.lock') + def clean_hyperopt(self): """ Remove hyperopt pickle files to restart hyperopt. """ - for f in [TICKERDATA_PICKLE, TRIALSDATA_PICKLE]: + for f in [self.tickerdata_pickle, self.trials_file]: p = Path(f) if p.is_file(): logger.info(f"Removing `{p}`.") @@ -199,7 +202,7 @@ class Hyperopt(Backtesting): if self.has_space('stoploss'): self.strategy.stoploss = params['stoploss'] - processed = load(TICKERDATA_PICKLE) + processed = load(self.tickerdata_pickle) min_date, max_date = get_timeframe(processed) @@ -305,7 +308,7 @@ class Hyperopt(Backtesting): preprocessed = self.strategy.tickerdata_to_dataframe(data) - dump(preprocessed, TICKERDATA_PICKLE) + dump(preprocessed, self.tickerdata_pickle) # We don't need exchange instance anymore while running hyperopt self.exchange = None # type: ignore diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 2370e145e..dc34198cf 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -15,8 +15,7 @@ from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss -from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, - Hyperopt) +from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType @@ -272,7 +271,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None: def test_start_filelock(mocker, default_conf, caplog) -> None: - start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) + start_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(default_conf))) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) @@ -609,10 +608,10 @@ def test_clean_hyperopt(mocker, default_conf, caplog): }) mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) - Hyperopt(default_conf) + h = Hyperopt(default_conf) assert unlinkmock.call_count == 2 - assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog.record_tuples) + assert log_has(f"Removing `{h.tickerdata_pickle}`.", caplog.record_tuples) def test_continue_hyperopt(mocker, default_conf, caplog): From 432b106d58182688606a558de62f1899f1394e0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 16:19:31 +0200 Subject: [PATCH 012/269] Improve docstring, remove unneeded method --- freqtrade/configuration/configuration.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 78584ce6a..a957e6e2a 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -124,7 +124,9 @@ class Configuration(object): setup_logging(config) - def _process_strategy_options(self, config: Dict[str, Any]) -> None: + def _process_common_options(self, config: Dict[str, Any]) -> None: + + self._process_logging_options(config) # 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'): @@ -133,11 +135,6 @@ class Configuration(object): self._args_to_config(config, argname='strategy_path', logstring='Using additional Strategy lookup path: {}') - def _process_common_options(self, config: Dict[str, Any]) -> None: - - self._process_logging_options(config) - self._process_strategy_options(config) - if ('db_url' in self.args and self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL): config.update({'db_url': self.args.db_url}) @@ -171,8 +168,8 @@ class Configuration(object): def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ - Extract information for sys.argv and load datadir configuration: - the --datadir option + Extract information for sys.argv and load directory configurations + --user-data, --datadir """ if 'user_data_dir' in self.args and self.args.user_data_dir: config.update({'user_data_dir': self.args.user_data_dir}) @@ -181,7 +178,6 @@ class Configuration(object): config.update({'user_data_dir': str(Path.cwd() / "user_data")}) # reset to user_data_dir so this contains the absolute path. config['user_data_dir'] = create_userdata_dir(config['user_data_dir']) - logger.info('Using user-data directory: %s ...', config['user_data_dir']) if 'datadir' in self.args and self.args.datadir: From 9de8d7276e170325a0cb75442b131dd95e12a1f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 07:16:52 +0200 Subject: [PATCH 013/269] have strategyresolver use user_data_dir --- freqtrade/resolvers/iresolver.py | 4 ++-- freqtrade/resolvers/strategy_resolver.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 9d0c97917..5a62d448f 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -57,7 +57,7 @@ class IResolver(object): if not str(entry).endswith('.py'): logger.debug('Ignoring %s', entry) continue - module_path = Path.resolve(directory.joinpath(entry)) + module_path = entry.resolve() obj = IResolver._get_valid_object( object_type, module_path, object_name ) @@ -84,6 +84,6 @@ class IResolver(object): f"from '{module_path}'...") return module except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.resolve()) return None diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index aa73327ff..37aa96b68 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -123,7 +123,7 @@ class StrategyResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() abs_paths = [ - Path.cwd().joinpath('user_data/strategies'), + config['user_data_dir'].joinpath('strategies'), current_path, ] From 333413d29846b4f505f580096ba1b916e7b0006f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 07:17:25 +0200 Subject: [PATCH 014/269] Add default_conf to strategy tests --- freqtrade/tests/strategy/test_strategy.py | 166 +++++++++++----------- 1 file changed, 85 insertions(+), 81 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index df8c0f126..c5fcd46d9 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -64,15 +64,18 @@ def test_search_strategy(): assert s is None -def test_load_strategy(result): - resolver = StrategyResolver({'strategy': 'TestStrategy'}) +def test_load_strategy(default_conf, result): + default_conf.update({'strategy': 'TestStrategy'}) + resolver = StrategyResolver(default_conf) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) -def test_load_strategy_base64(result, caplog): +def test_load_strategy_base64(result, caplog, default_conf): with open("user_data/strategies/test_strategy.py", "rb") as file: encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") - resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) + default_conf.update({'strategy': 'TestStrategy:{}'.format(encoded_string)}) + + resolver = StrategyResolver(default_conf) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) # Make sure strategy was loaded from base64 (using temp directory)!! assert log_has_re(r"Using resolved strategy TestStrategy from '" @@ -80,47 +83,47 @@ def test_load_strategy_base64(result, caplog): caplog.record_tuples) -def test_load_strategy_invalid_directory(result, caplog): - resolver = StrategyResolver() +def test_load_strategy_invalid_directory(result, caplog, default_conf): + resolver = StrategyResolver(default_conf) extra_dir = Path.cwd() / 'some/path' - resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) + resolver._load_strategy('TestStrategy', config=default_conf, extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) -def test_load_not_found_strategy(): - strategy = StrategyResolver() +def test_load_not_found_strategy(default_conf): + strategy = StrategyResolver(default_conf) with pytest.raises(OperationalException, match=r"Impossible to load Strategy 'NotFoundStrategy'. " r"This class does not exist or contains Python code errors."): - strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) + strategy._load_strategy(strategy_name='NotFoundStrategy', config=default_conf) -def test_load_staticmethod_importerror(mocker, caplog): +def test_load_staticmethod_importerror(mocker, caplog, default_conf): mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock( side_effect=TypeError("can't pickle staticmethod objects"))) with pytest.raises(OperationalException, match=r"Impossible to load Strategy 'DefaultStrategy'. " r"This class does not exist or contains Python code errors."): - StrategyResolver() + StrategyResolver(default_conf) assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples) -def test_strategy(result): - config = {'strategy': 'DefaultStrategy'} +def test_strategy(result, default_conf): + default_conf.update({'strategy': 'DefaultStrategy'}) - resolver = StrategyResolver(config) + resolver = StrategyResolver(default_conf) metadata = {'pair': 'ETH/BTC'} assert resolver.strategy.minimal_roi[0] == 0.04 - assert config["minimal_roi"]['0'] == 0.04 + assert default_conf["minimal_roi"]['0'] == 0.04 assert resolver.strategy.stoploss == -0.10 - assert config['stoploss'] == -0.10 + assert default_conf['stoploss'] == -0.10 assert resolver.strategy.ticker_interval == '5m' - assert config['ticker_interval'] == '5m' + assert default_conf['ticker_interval'] == '5m' df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators @@ -132,15 +135,15 @@ def test_strategy(result): assert 'sell' in dataframe.columns -def test_strategy_override_minimal_roi(caplog): +def test_strategy_override_minimal_roi(caplog, default_conf): caplog.set_level(logging.INFO) - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'minimal_roi': { "0": 0.5 } - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.minimal_roi[0] == 0.5 assert ('freqtrade.resolvers.strategy_resolver', @@ -149,13 +152,13 @@ def test_strategy_override_minimal_roi(caplog): ) in caplog.record_tuples -def test_strategy_override_stoploss(caplog): +def test_strategy_override_stoploss(caplog, default_conf): caplog.set_level(logging.INFO) - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'stoploss': -0.5 - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.stoploss == -0.5 assert ('freqtrade.resolvers.strategy_resolver', @@ -164,13 +167,13 @@ def test_strategy_override_stoploss(caplog): ) in caplog.record_tuples -def test_strategy_override_trailing_stop(caplog): +def test_strategy_override_trailing_stop(caplog, default_conf): caplog.set_level(logging.INFO) - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'trailing_stop': True - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.trailing_stop assert isinstance(resolver.strategy.trailing_stop, bool) @@ -180,15 +183,15 @@ def test_strategy_override_trailing_stop(caplog): ) in caplog.record_tuples -def test_strategy_override_trailing_stop_positive(caplog): +def test_strategy_override_trailing_stop_positive(caplog, default_conf): caplog.set_level(logging.INFO) - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'trailing_stop_positive': -0.1, 'trailing_stop_positive_offset': -0.2 - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.trailing_stop_positive == -0.1 assert ('freqtrade.resolvers.strategy_resolver', @@ -203,15 +206,15 @@ def test_strategy_override_trailing_stop_positive(caplog): ) in caplog.record_tuples -def test_strategy_override_ticker_interval(caplog): +def test_strategy_override_ticker_interval(caplog, default_conf): caplog.set_level(logging.INFO) - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'ticker_interval': 60, 'stake_currency': 'ETH' - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.ticker_interval == 60 assert resolver.strategy.stake_currency == 'ETH' @@ -221,14 +224,14 @@ def test_strategy_override_ticker_interval(caplog): ) in caplog.record_tuples -def test_strategy_override_process_only_new_candles(caplog): +def test_strategy_override_process_only_new_candles(caplog, default_conf): caplog.set_level(logging.INFO) - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'process_only_new_candles': True - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.process_only_new_candles assert ('freqtrade.resolvers.strategy_resolver', @@ -237,7 +240,7 @@ def test_strategy_override_process_only_new_candles(caplog): ) in caplog.record_tuples -def test_strategy_override_order_types(caplog): +def test_strategy_override_order_types(caplog, default_conf): caplog.set_level(logging.INFO) order_types = { @@ -246,12 +249,11 @@ def test_strategy_override_order_types(caplog): 'stoploss': 'limit', 'stoploss_on_exchange': True, } - - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'order_types': order_types - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.order_types for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: @@ -264,18 +266,18 @@ def test_strategy_override_order_types(caplog): " 'stoploss_on_exchange': True}." ) in caplog.record_tuples - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'order_types': {'buy': 'market'} - } + }) # Raise error for invalid configuration with pytest.raises(ImportError, match=r"Impossible to load Strategy 'DefaultStrategy'. " r"Order-types mapping is incomplete."): - StrategyResolver(config) + StrategyResolver(default_conf) -def test_strategy_override_order_tif(caplog): +def test_strategy_override_order_tif(caplog, default_conf): caplog.set_level(logging.INFO) order_time_in_force = { @@ -283,11 +285,11 @@ def test_strategy_override_order_tif(caplog): 'sell': 'gtc', } - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'order_time_in_force': order_time_in_force - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.order_time_in_force for method in ['buy', 'sell']: @@ -299,36 +301,36 @@ def test_strategy_override_order_tif(caplog): " {'buy': 'fok', 'sell': 'gtc'}." ) in caplog.record_tuples - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'order_time_in_force': {'buy': 'fok'} - } + }) # Raise error for invalid configuration with pytest.raises(ImportError, match=r"Impossible to load Strategy 'DefaultStrategy'. " r"Order-time-in-force mapping is incomplete."): - StrategyResolver(config) + StrategyResolver(default_conf) -def test_strategy_override_use_sell_signal(caplog): +def test_strategy_override_use_sell_signal(caplog, default_conf): caplog.set_level(logging.INFO) - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert not resolver.strategy.use_sell_signal assert isinstance(resolver.strategy.use_sell_signal, bool) # must be inserted to configuration - assert 'use_sell_signal' in config['experimental'] - assert not config['experimental']['use_sell_signal'] + assert 'use_sell_signal' in default_conf['experimental'] + assert not default_conf['experimental']['use_sell_signal'] - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'experimental': { 'use_sell_signal': True, }, - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.use_sell_signal assert isinstance(resolver.strategy.use_sell_signal, bool) @@ -338,25 +340,25 @@ def test_strategy_override_use_sell_signal(caplog): ) in caplog.record_tuples -def test_strategy_override_use_sell_profit_only(caplog): +def test_strategy_override_use_sell_profit_only(caplog, default_conf): caplog.set_level(logging.INFO) - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert not resolver.strategy.sell_profit_only assert isinstance(resolver.strategy.sell_profit_only, bool) # must be inserted to configuration - assert 'sell_profit_only' in config['experimental'] - assert not config['experimental']['sell_profit_only'] + assert 'sell_profit_only' in default_conf['experimental'] + assert not default_conf['experimental']['sell_profit_only'] - config = { + default_conf.update({ 'strategy': 'DefaultStrategy', 'experimental': { 'sell_profit_only': True, }, - } - resolver = StrategyResolver(config) + }) + resolver = StrategyResolver(default_conf) assert resolver.strategy.sell_profit_only assert isinstance(resolver.strategy.sell_profit_only, bool) @@ -367,10 +369,11 @@ def test_strategy_override_use_sell_profit_only(caplog): @pytest.mark.filterwarnings("ignore:deprecated") -def test_deprecate_populate_indicators(result): +def test_deprecate_populate_indicators(result, default_conf): default_location = path.join(path.dirname(path.realpath(__file__))) - resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', - 'strategy_path': default_location}) + default_conf.update({'strategy': 'TestStrategyLegacy', + 'strategy_path': default_location}) + resolver = StrategyResolver(default_conf) with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") @@ -400,10 +403,11 @@ def test_deprecate_populate_indicators(result): @pytest.mark.filterwarnings("ignore:deprecated") -def test_call_deprecated_function(result, monkeypatch): +def test_call_deprecated_function(result, monkeypatch, default_conf): default_location = path.join(path.dirname(path.realpath(__file__))) - resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', - 'strategy_path': default_location}) + default_conf.update({'strategy': 'TestStrategyLegacy', + 'strategy_path': default_location}) + resolver = StrategyResolver(default_conf) metadata = {'pair': 'ETH/BTC'} # Make sure we are using a legacy function From a3c605f147d6c88dd027d2ba4f4b2758a75370ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jul 2019 14:49:34 +0200 Subject: [PATCH 015/269] PairListResovler to use user_data_dir --- freqtrade/resolvers/pairlist_resolver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 3d95c0295..2c88f27a3 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -25,11 +25,11 @@ class PairListResolver(IResolver): Load the custom class from config parameter :param config: configuration dictionary or None """ - self.pairlist = self._load_pairlist(pairlist_name, kwargs={'freqtrade': freqtrade, - 'config': config}) + self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'freqtrade': freqtrade, + 'config': config}) def _load_pairlist( - self, pairlist_name: str, kwargs: dict) -> IPairList: + self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList: """ Search and loads the specified pairlist. :param pairlist_name: name of the module to import @@ -39,7 +39,7 @@ class PairListResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() abs_paths = [ - Path.cwd().joinpath('user_data/pairlist'), + config['user_data_dir'].joinpath('user_data/pairlist'), current_path, ] From 14b43b504b8e672e7b7be17c91b2bb1e06880232 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jul 2019 15:02:11 +0200 Subject: [PATCH 016/269] Use user_data_dir for hyperopt --- freqtrade/resolvers/hyperopt_resolver.py | 18 +++++++++++------- freqtrade/resolvers/pairlist_resolver.py | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 5027d7ddf..15d1997ef 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -31,7 +31,8 @@ class HyperOptResolver(IResolver): # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT - self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) + self.hyperopt = self._load_hyperopt(hyperopt_name, config, + extra_dir=config.get('hyperopt_path')) # Assign ticker_interval to be used in hyperopt self.hyperopt.__class__.ticker_interval = str(config['ticker_interval']) @@ -44,17 +45,18 @@ class HyperOptResolver(IResolver): "Using populate_sell_trend from DefaultStrategy.") def _load_hyperopt( - self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt: + self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt: """ Search and loads the specified hyperopt. :param hyperopt_name: name of the module to import + :param config: configuration dictionary :param extra_dir: additional directory to search for the given hyperopt :return: HyperOpt instance or None """ current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = [ - Path.cwd().joinpath('user_data/hyperopts'), + config['user_data_dir'].joinpath('hyperopts'), current_path, ] @@ -79,7 +81,7 @@ class HyperOptLossResolver(IResolver): __slots__ = ['hyperoptloss'] - def __init__(self, config: Optional[Dict] = None) -> None: + def __init__(self, config: Dict = None) -> None: """ Load the custom class from config parameter :param config: configuration dictionary or None @@ -89,7 +91,7 @@ class HyperOptLossResolver(IResolver): # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS self.hyperoptloss = self._load_hyperoptloss( - hyperopt_name, extra_dir=config.get('hyperopt_path')) + hyperopt_name, config, extra_dir=config.get('hyperopt_path')) # Assign ticker_interval to be used in hyperopt self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) @@ -99,17 +101,19 @@ class HyperOptLossResolver(IResolver): f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.") def _load_hyperoptloss( - self, hyper_loss_name: str, extra_dir: Optional[str] = None) -> IHyperOptLoss: + self, hyper_loss_name: str, config: Dict, + extra_dir: Optional[str] = None) -> IHyperOptLoss: """ Search and loads the specified hyperopt loss class. :param hyper_loss_name: name of the module to import + :param config: configuration dictionary :param extra_dir: additional directory to search for the given hyperopt :return: HyperOptLoss instance or None """ current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = [ - Path.cwd().joinpath('user_data/hyperopts'), + config['user_data_dir'].joinpath('hyperopts'), current_path, ] diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 2c88f27a3..f38253155 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -33,13 +33,14 @@ class PairListResolver(IResolver): """ Search and loads the specified pairlist. :param pairlist_name: name of the module to import + :param config: configuration dictionary :param extra_dir: additional directory to search for the given pairlist :return: PairList instance or None """ current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() abs_paths = [ - config['user_data_dir'].joinpath('user_data/pairlist'), + config['user_data_dir'].joinpath('pairlist'), current_path, ] From 73ac98da8053202a39822f7b01d98f276045d03a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jul 2019 15:11:41 +0200 Subject: [PATCH 017/269] Small fixes while tsting --- freqtrade/configuration/configuration.py | 1 + freqtrade/configuration/folder_operations.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index a957e6e2a..cb383d8b6 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -176,6 +176,7 @@ class Configuration(object): elif 'user_data_dir' not in config: # Default to cwd/user_data (legacy option ...) config.update({'user_data_dir': str(Path.cwd() / "user_data")}) + # reset to user_data_dir so this contains the absolute path. config['user_data_dir'] = create_userdata_dir(config['user_data_dir']) logger.info('Using user-data directory: %s ...', config['user_data_dir']) diff --git a/freqtrade/configuration/folder_operations.py b/freqtrade/configuration/folder_operations.py index 2ea9cd268..fc516a8cc 100644 --- a/freqtrade/configuration/folder_operations.py +++ b/freqtrade/configuration/folder_operations.py @@ -20,8 +20,8 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str return str(folder) -def create_userdata_dir(directory: str) -> str: - sub_dirs = ["backtest_results", "data", "hyperopts", "plots", "strategies", ] +def create_userdata_dir(directory: str) -> Path: + sub_dirs = ["backtest_results", "data", "hyperopts", "plot", "strategies", ] folder = Path(directory) if not folder.is_dir(): folder.mkdir(parents=True) @@ -32,5 +32,4 @@ def create_userdata_dir(directory: str) -> str: subfolder = folder / f if not subfolder.is_dir(): subfolder.mkdir(parents=False) - # TODO: convert this to return Path return folder From c1bc1e31373f2159c650a1f0d7580f0ddf26f675 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jul 2019 15:34:49 +0200 Subject: [PATCH 018/269] Add documentation for user_data_dir --- docs/bot-usage.md | 50 ++++++++++++++++++++++++++++++++----------- docs/configuration.md | 1 + 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index ef28eb60c..f407e6a23 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -9,38 +9,43 @@ This page explains the different parameters of the bot and how to run it. ## Bot commands ``` -usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] - [-s NAME] [--strategy-path PATH] [--db-url PATH] - [--sd-notify] - {backtesting,edge,hyperopt} ... +usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + [--db-url PATH] [--sd-notify] + {backtesting,edge,hyperopt,create-userdir,list-exchanges} ... Free, open source crypto trading bot positional arguments: - {backtesting,edge,hyperopt} + {backtesting,edge,hyperopt,create-userdir,list-exchanges} backtesting Backtesting module. edge Edge module. hyperopt Hyperopt module. + create-userdir Create user-data directory. + list-exchanges Print available exchanges. optional arguments: -h, --help show this help message and exit -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified + --logfile FILE Log to the file specified. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: None). Multiple - --config options may be used. Can be set to '-' to - read config from stdin. + Specify configuration file (default: `config.json`). + Multiple --config options may be used. Can be set to + `-` to read config from stdin. -d PATH, --datadir PATH Path to backtest data. + --userdir PATH, --user-data-dir PATH + Path to Userdata Directory. -s NAME, --strategy NAME Specify strategy class name (default: - DefaultStrategy). + `DefaultStrategy`). --strategy-path PATH Specify additional strategy lookup path. - --db-url PATH Override trades database URL, this is useful if - dry_run is enabled or in custom deployments (default: - None). + --db-url PATH Override trades database URL, this is useful in custom + deployments (default: `sqlite:///tradesv3.sqlite` for + Live Run mode, `sqlite://` for Dry Run). --sd-notify Notify systemd service manager. + ``` ### How to use a different configuration file? @@ -82,6 +87,25 @@ of your configuration in the project issues or in the Internet. See more details on this technique with examples in the documentation page on [configuration](configuration.md). +### Where to store custom data + +Freqtrade allows the creation of a user-data directory using `freqtrade create-userdir --userdir someDirectory`. +This directory will look as follows: + +user_data/ +├── backtest_results +├── data +├── hyperopts +├── plot +└── strategies + +You can add the entry "user_data_dir" to your configuration, to always point your bot to this folder. +Alternatively, pass in `--userdir` to every command. + +This directory should contain your custom strategies, custom hyperopts, backtest data (downloaded using either backtesting or the download script) and plot outputs. + +It is reccomendet to use version control to keep track of changes to your strategies. + ### How to use **--strategy**? This parameter will allow you to load your custom strategy class. diff --git a/docs/configuration.md b/docs/configuration.md index f8dbbbbbb..34d0e110f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -71,6 +71,7 @@ Mandatory Parameters are marked as **Required**. | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. +| `user_data_dir` | cwd()/user_data | Folder containing user data. Defaults to `./user_data/`. ### Parameters in the strategy From 03e60b9ea490d852bea26797a575d82dd6d49b7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 06:15:49 +0200 Subject: [PATCH 019/269] Rename folder_Operations to directory_operations --- freqtrade/configuration/configuration.py | 2 +- .../{folder_operations.py => directory_operations.py} | 0 freqtrade/tests/test_configuration.py | 2 +- freqtrade/utils.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename freqtrade/configuration/{folder_operations.py => directory_operations.py} (100%) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index cb383d8b6..e56c2c06a 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -11,7 +11,7 @@ from typing import Any, Callable, Dict, Optional from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.folder_operations import create_datadir, create_userdata_dir +from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts diff --git a/freqtrade/configuration/folder_operations.py b/freqtrade/configuration/directory_operations.py similarity index 100% rename from freqtrade/configuration/folder_operations.py rename to freqtrade/configuration/directory_operations.py diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 3d75818d4..2b967a859 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -13,7 +13,7 @@ from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants from freqtrade.configuration import Arguments, Configuration from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.folder_operations import create_datadir, create_userdata_dir +from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 53325b70b..fa6bc2d1d 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -4,7 +4,7 @@ from argparse import Namespace from typing import Any, Dict from freqtrade.configuration import Configuration -from freqtrade.configuration.folder_operations import create_userdata_dir +from freqtrade.configuration.directory_operations import create_userdata_dir from freqtrade.exchange import available_exchanges from freqtrade.state import RunMode From 7a97995d81d25f6f9928c82a31d8de57c36e6a8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:30:14 +0200 Subject: [PATCH 020/269] 2017.7-dev version bump --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 10900bee9..14f0bb819 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.7' +__version__ = '2019.7-dev' class DependencyException(Exception): From e14dd4974f0ebfeb07a302064cc2fc5cd615019d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:32:28 +0200 Subject: [PATCH 021/269] Improve release documentation --- docs/developer.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index f58e0597d..f50625111 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -156,6 +156,8 @@ git log --oneline --no-decorate --no-merges master..develop ### Create github release / tag +Once the PR against master is merged (best right after merging): + * Use the button "Draft a new release" in the Github UI (subsection releases) * Use the version-number specified as tag. * Use "master" as reference (this step comes after the above PR is merged). From 8dd8addd3aeadfd652f65a3a141491e263dcd79b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:52:38 +0200 Subject: [PATCH 022/269] Sort requirements-dev file --- requirements-dev.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 946d63039..f54b38a57 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,13 +2,13 @@ -r requirements.txt -r requirements-plot.txt +coveralls==1.8.1 flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 +mypy==0.720 pytest==5.0.1 -pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 +pytest-mock==1.10.4 pytest-random-order==1.0.4 -coveralls==1.8.1 -mypy==0.720 From 7bea0007c7b6ef253a5e7903728ac556f5c2e0e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:53:26 +0200 Subject: [PATCH 023/269] Allow installing via submodules freqtrade can be installed using `pip install -e .[all]` to include all dependencies --- setup.py | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index ca2f81d1f..1ae395295 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,24 @@ if version_info.major == 3 and version_info.minor < 6 or \ from freqtrade import __version__ +# Requirements used for submodules +api = ['flask'] +plot = ['plotly>=4.0'] + +develop = [ + 'coveralls', + 'flake8', + 'flake8-type-annotations', + 'flake8-tidy-imports', + 'mypy', + 'pytest', + 'pytest-asyncio', + 'pytest-cov', + 'pytest-mock', + 'pytest-random-order', +] + +all_extra = api + plot + develop setup(name='freqtrade', version=__version__, @@ -20,26 +38,37 @@ setup(name='freqtrade', setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ - 'ccxt', + # from requirements-common.txt + 'ccxt>=1.18', 'SQLAlchemy', 'python-telegram-bot', 'arrow', + 'cachetools', 'requests', 'urllib3', 'wrapt', - 'pandas', 'scikit-learn', - 'scipy', 'joblib', 'jsonschema', 'TA-Lib', 'tabulate', - 'cachetools', 'coinmarketcap', 'scikit-optimize', + 'filelock', + 'py_find_1st', 'python-rapidjson', - 'py_find_1st' + 'sdnotify', + # from requirements.txt + 'numpy', + 'pandas', + 'scipy', ], + extras_require={ + 'api': api, + 'dev': develop, + 'plot': plot, + 'all': all_extra, + }, include_package_data=True, zip_safe=False, entry_points={ From f825e81d0eba06b58adc721715d7bbc545413ee4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:54:35 +0200 Subject: [PATCH 024/269] developers need all dependencies! --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1ae395295..202e3fd0d 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ setup(name='freqtrade', ], extras_require={ 'api': api, - 'dev': develop, + 'dev': all_extra, 'plot': plot, 'all': all_extra, }, From 59caff8fb1d49bf26b29cb3855354d0eed7d2aa9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:57:57 +0200 Subject: [PATCH 025/269] UPdate developer docs --- docs/developer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index f58e0597d..2895db7e8 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -12,8 +12,8 @@ Special fields for the documentation (like Note boxes, ...) can be found [here]( ## Developer setup -To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". -Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`. +To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". +Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. From 8f1f416a52b2381d6c2958a905eb4716dae1d037 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 30 Jul 2019 11:47:28 +0300 Subject: [PATCH 026/269] hyperopt cleanup and output improvements --- freqtrade/optimize/hyperopt.py | 65 ++++++++++++++++------------------ 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 759ceffbe..842a111ca 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -51,7 +51,7 @@ class Hyperopt(Backtesting): self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function - self.total_tries = config.get('epochs', 0) + self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 if not self.config.get('hyperopt_continue'): @@ -124,13 +124,12 @@ class Hyperopt(Backtesting): """ results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] - logger.info( - 'Best result:\n%s\nwith values:\n', - best_result['result'] - ) + + log_str = self.format_results_logstring(best_result) + print(f"\nBest result:\n{log_str}\nwith values:") pprint(best_result['params'], indent=4) if 'roi_t1' in best_result['params']: - logger.info('ROI table:') + print("ROI table:") pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4) def log_results(self, results) -> None: @@ -139,22 +138,26 @@ class Hyperopt(Backtesting): """ print_all = self.config.get('print_all', False) if print_all or results['loss'] < self.current_best_loss: - # Output human-friendly index here (starting from 1) - current = results['current_tries'] + 1 - total = results['total_tries'] - res = results['result'] - loss = results['loss'] - self.current_best_loss = results['loss'] - log_msg = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' - log_msg = f'*{log_msg}' if results['initial_point'] else f' {log_msg}' + log_str = self.format_results_logstring(results) if print_all: - print(log_msg) + print(log_str) else: - print('\n' + log_msg) + print('\n' + log_str) else: print('.', end='') sys.stdout.flush() + def format_results_logstring(self, results) -> str: + # Output human-friendly index here (starting from 1) + current = results['current_epoch'] + 1 + total = self.total_epochs + res = results['results_explanation'] + loss = results['loss'] + self.current_best_loss = results['loss'] + log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' + log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}' + return log_str + def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration @@ -214,7 +217,7 @@ class Hyperopt(Backtesting): 'end_date': max_date, } ) - result_explanation = self.format_results(results) + results_explanation = self.format_results(results) trade_count = len(results.index) @@ -226,7 +229,7 @@ class Hyperopt(Backtesting): return { 'loss': MAX_LOSS, 'params': params, - 'result': result_explanation, + 'results_explanation': results_explanation, } loss = self.calculate_loss(results=results, trade_count=trade_count, @@ -235,12 +238,12 @@ class Hyperopt(Backtesting): return { 'loss': loss, 'params': params, - 'result': result_explanation, + 'results_explanation': results_explanation, } def format_results(self, results: DataFrame) -> str: """ - Return the format result in a string + Return the formatted results explanation in a string """ trades = len(results.index) avg_profit = results.profit_percent.mean() * 100.0 @@ -323,25 +326,19 @@ class Hyperopt(Backtesting): with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() logger.info(f'Effective number of parallel workers used: {jobs}') - EVALS = max(self.total_tries // jobs, 1) + EVALS = max(self.total_epochs // jobs, 1) for i in range(EVALS): asked = opt.ask(n_points=jobs) f_val = self.run_optimizer_parallel(parallel, asked) - opt.tell(asked, [i['loss'] for i in f_val]) - - self.trials += f_val + opt.tell(asked, [v['loss'] for v in f_val]) for j in range(jobs): current = i * jobs + j - self.log_results({ - 'loss': f_val[j]['loss'], - 'current_tries': current, - 'initial_point': current < INITIAL_POINTS, - 'total_tries': self.total_tries, - 'result': f_val[j]['result'], - }) - logger.debug(f"Optimizer params: {f_val[j]['params']}") - for j in range(jobs): - logger.debug(f"Optimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}") + val = f_val[j] + val['current_epoch'] = current + val['is_initial_point'] = current < INITIAL_POINTS + self.log_results(val) + self.trials.append(val) + logger.debug(f"Optimizer epoch evaluated: {val}") except KeyboardInterrupt: print('User interrupted..') From b976f24672fd8f0118d94a5890925fc404998abc Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 30 Jul 2019 11:47:46 +0300 Subject: [PATCH 027/269] tests adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 065b4c41b..5d6128e7c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -370,13 +370,13 @@ def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 + hyperopt.total_epochs = 2 hyperopt.log_results( { 'loss': 1, - 'current_tries': 1, - 'total_tries': 2, - 'result': 'foo.', - 'initial_point': False + 'current_epoch': 1, + 'results_explanation': 'foo.', + 'is_initial_point': False } ) out, err = capsys.readouterr() @@ -433,7 +433,7 @@ def test_roi_table_generation(hyperopt) -> None: assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} -def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: +def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch( @@ -443,7 +443,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: parallel = mocker.patch( 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', - MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}]) + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) ) patch_exchange(mocker) @@ -457,8 +457,11 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.start() + parallel.assert_called_once() - assert log_has('Best result:\nfoo result\nwith values:\n', caplog.record_tuples) + + out, err = capsys.readouterr() + assert 'Best result:\n* 1/1: foo result Objective: 1.00000\nwith values:\n' in out assert dumper.called # Should be called twice, once for tickerdata, once to save evaluations assert dumper.call_count == 2 @@ -598,8 +601,8 @@ def test_generate_optimizer(mocker, default_conf) -> None: } response_expected = { 'loss': 1.9840569076926293, - 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' - '( 2.31Σ%). Avg duration 100.0 mins.', + 'results_explanation': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' + '( 2.31Σ%). Avg duration 100.0 mins.', 'params': optimizer_param } From 0488525888d391a651fd4dcb60ded8c799a4c6ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 31 Jul 2019 06:49:25 +0200 Subject: [PATCH 028/269] Fix some documentation errors --- docs/bot-usage.md | 12 +++++++----- freqtrade/configuration/cli_options.py | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index f407e6a23..1b66cd840 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -34,9 +34,9 @@ optional arguments: Multiple --config options may be used. Can be set to `-` to read config from stdin. -d PATH, --datadir PATH - Path to backtest data. + Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH - Path to Userdata Directory. + Path to userdata directory. -s NAME, --strategy NAME Specify strategy class name (default: `DefaultStrategy`). @@ -92,19 +92,21 @@ See more details on this technique with examples in the documentation page on Freqtrade allows the creation of a user-data directory using `freqtrade create-userdir --userdir someDirectory`. This directory will look as follows: +``` user_data/ ├── backtest_results ├── data ├── hyperopts ├── plot └── strategies +``` -You can add the entry "user_data_dir" to your configuration, to always point your bot to this folder. +You can add the entry "user_data_dir" setting to your configuration, to always point your bot to this directory. Alternatively, pass in `--userdir` to every command. -This directory should contain your custom strategies, custom hyperopts, backtest data (downloaded using either backtesting or the download script) and plot outputs. +This directory should contain your custom strategies, custom hyperopts and hyperopt loss functions, backtesting historical data (downloaded using either backtesting command or the download script) and plot outputs. -It is reccomendet to use version control to keep track of changes to your strategies. +It is recommended to use version control to keep track of changes to your strategies. ### How to use **--strategy**? diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 00c28ed07..438e6c5bc 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -55,12 +55,12 @@ AVAILABLE_CLI_OPTIONS = { ), "datadir": Arg( '-d', '--datadir', - help='Path to backtest data.', + help='Path to directory with historical backtesting data.', metavar='PATH', ), "user_data_dir": Arg( '--userdir', '--user-data-dir', - help='Path to Userdata Directory.', + help='Path to userdata directory.', metavar='PATH', ), # Main options From c3d14ab9b93071a0167a544d7e2cf4a0bf2ae282 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 31 Jul 2019 06:54:45 +0200 Subject: [PATCH 029/269] don't use "folder" ... --- docs/configuration.md | 2 +- freqtrade/plot/plotting.py | 6 +++--- scripts/plot_dataframe.py | 2 +- scripts/plot_profit.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 34d0e110f..dfe191b10 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -71,7 +71,7 @@ Mandatory Parameters are marked as **Required**. | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. -| `user_data_dir` | cwd()/user_data | Folder containing user data. Defaults to `./user_data/`. +| `user_data_dir` | cwd()/user_data | Directory containing user data. Defaults to `./user_data/`. ### Parameters in the strategy diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 948964462..f2c999369 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -308,7 +308,7 @@ def generate_plot_filename(pair, ticker_interval) -> str: return file_name -def store_plot_file(fig, filename: str, folder: Path, auto_open: bool = False) -> None: +def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False) -> None: """ Generate a plot html file from pre populated fig plotly object :param fig: Plotly Figure to plot @@ -317,7 +317,7 @@ def store_plot_file(fig, filename: str, folder: Path, auto_open: bool = False) - :return: None """ - folder.mkdir(parents=True, exist_ok=True) + directory.mkdir(parents=True, exist_ok=True) - plot(fig, filename=str(folder.joinpath(filename)), + plot(fig, filename=str(directory.joinpath(filename)), auto_open=auto_open) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 04911f93e..e1548754a 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -78,7 +78,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): ) store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']), - folder=config['user_data_dir'] / "plot") + directory=config['user_data_dir'] / "plot") logger.info('End of ploting process %s plots generated', pair_counter) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index c83ad1088..578ddf15f 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -33,7 +33,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades) store_plot_file(fig, filename='freqtrade-profit-plot.html', - folder=config['user_data_dir'] / "plot", auto_open=True) + directory=config['user_data_dir'] / "plot", auto_open=True) def plot_parse_args(args: List[str]) -> Dict[str, Any]: From 5d22d541f21e67045ca2c125f88d5e1f9652b653 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 31 Jul 2019 06:58:26 +0200 Subject: [PATCH 030/269] Add forgotten directory --- freqtrade/tests/test_plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index c80d0e780..83a561110 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -211,7 +211,7 @@ def test_generate_plot_file(mocker, caplog): fig = generage_empty_figure() plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html", - folder=Path("user_data/plots")) + directory=Path("user_data/plots")) assert plot_mock.call_count == 1 assert plot_mock.call_args[0][0] == fig From 8cef567abcfea2fd3a1e0fac09e62f411696f817 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 31 Jul 2019 07:07:46 +0200 Subject: [PATCH 031/269] create and use hyperopt-results folder --- docs/bot-usage.md | 1 + freqtrade/configuration/directory_operations.py | 2 +- freqtrade/optimize/hyperopt.py | 6 ++++-- freqtrade/tests/test_configuration.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 1b66cd840..130c55518 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -97,6 +97,7 @@ user_data/ ├── backtest_results ├── data ├── hyperopts +├── hyperopts_results ├── plot └── strategies ``` diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index fc516a8cc..7542c2b80 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -21,7 +21,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str def create_userdata_dir(directory: str) -> Path: - sub_dirs = ["backtest_results", "data", "hyperopts", "plot", "strategies", ] + sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "plot", "strategies", ] folder = Path(directory) if not folder.is_dir(): folder.mkdir(parents=True) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 2fff91e58..ecf14def9 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -47,8 +47,10 @@ class Hyperopt(Backtesting): self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function - self.trials_file = self.config['user_data_dir'] / 'hyperopt_results.pickle' - self.tickerdata_pickle = self.config['user_data_dir'] / 'hyperopt_tickerdata.pkl' + self.trials_file = (self.config['user_data_dir'] / + 'hyperopt_results' / 'hyperopt_results.pickle') + self.tickerdata_pickle = (self.config['user_data_dir'] / + 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_tries = config.get('epochs', 0) self.current_best_loss = 100 diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 2b967a859..4dd3760db 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -626,7 +626,7 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None: md = mocker.patch.object(Path, 'mkdir', MagicMock()) x = create_userdata_dir('/tmp/bar') - assert md.call_count == 6 + assert md.call_count == 7 assert md.call_args[1]['parents'] is False assert log_has('Created user-data directory: /tmp/bar', caplog.record_tuples) assert isinstance(x, Path) From 2a141af42e538c825c28bc51c9cd303eb3c9feea Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 31 Jul 2019 19:39:54 +0200 Subject: [PATCH 032/269] Only create userdir when explicitly requested --- docs/bot-usage.md | 1 + freqtrade/configuration/configuration.py | 2 +- .../configuration/directory_operations.py | 21 ++++++++++++++++--- freqtrade/tests/test_configuration.py | 13 ++++++++++-- freqtrade/utils.py | 2 +- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 130c55518..647531d3a 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -104,6 +104,7 @@ user_data/ You can add the entry "user_data_dir" setting to your configuration, to always point your bot to this directory. Alternatively, pass in `--userdir` to every command. +The bot will fail to start if the directory does not exist, but will create necessary subdirectories. This directory should contain your custom strategies, custom hyperopts and hyperopt loss functions, backtesting historical data (downloaded using either backtesting command or the download script) and plot outputs. diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index e56c2c06a..a2ce54e8b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -178,7 +178,7 @@ class Configuration(object): config.update({'user_data_dir': str(Path.cwd() / "user_data")}) # reset to user_data_dir so this contains the absolute path. - config['user_data_dir'] = create_userdata_dir(config['user_data_dir']) + config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False) logger.info('Using user-data directory: %s ...', config['user_data_dir']) if 'datadir' in self.args and self.args.datadir: diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 7542c2b80..395accd90 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -2,6 +2,7 @@ import logging from typing import Any, Dict, Optional from pathlib import Path +from freqtrade import OperationalException logger = logging.getLogger(__name__) @@ -20,12 +21,26 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str return str(folder) -def create_userdata_dir(directory: str) -> Path: +def create_userdata_dir(directory: str, create_dir=False) -> Path: + """ + Create userdata directory structure. + if create_dir is True, then the parent-directory will be created if it does not exist. + Sub-directories will always be created if the parent directory exists. + Raises OperationalException if given a non-existing directory. + :param directory: Directory to check + :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", ] folder = Path(directory) if not folder.is_dir(): - folder.mkdir(parents=True) - logger.info(f'Created user-data directory: {folder}') + if create_dir: + folder.mkdir(parents=True) + logger.info(f'Created user-data directory: {folder}') + else: + raise OperationalException( + f"Directory `{folder}` does not exist. " + "Please use `freqtrade create-userdir` to create a user directory") # Create required subdirectories for f in sub_dirs: diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 4dd3760db..838578e25 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -335,7 +335,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non ) mocker.patch( 'freqtrade.configuration.configuration.create_userdata_dir', - lambda x: Path(x) + lambda x, *args, **kwargs: Path(x) ) arglist = [ '--config', 'config.json', @@ -625,7 +625,7 @@ 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') + 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('Created user-data directory: /tmp/bar', caplog.record_tuples) @@ -641,6 +641,15 @@ def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: 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 `/tmp/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['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0 diff --git a/freqtrade/utils.py b/freqtrade/utils.py index fa6bc2d1d..2c7902ebd 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -49,7 +49,7 @@ def start_create_userdir(args: Namespace) -> None: :return: None """ if "user_data_dir" in args and args.user_data_dir: - create_userdata_dir(args.user_data_dir) + create_userdata_dir(args.user_data_dir, create_dir=True) else: logger.warning("`create-userdir` requires --userdir to be set.") sys.exit(1) From 065ebd39ef9fae6c57f133bbf4c41ee46e3e6be1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 1 Aug 2019 23:57:26 +0300 Subject: [PATCH 033/269] cleanup in hyperopt --- freqtrade/optimize/hyperopt.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 842a111ca..479f8c70a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -78,6 +78,12 @@ class Hyperopt(Backtesting): self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False), + if self.has_space('sell'): + # Make sure experimental is enabled + if 'experimental' not in self.config: + self.config['experimental'] = {} + self.config['experimental']['use_sell_signal'] = True + def clean_hyperopt(self): """ Remove hyperopt pickle files to restart hyperopt. @@ -124,13 +130,14 @@ class Hyperopt(Backtesting): """ results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] + params = best_result['params'] log_str = self.format_results_logstring(best_result) print(f"\nBest result:\n{log_str}\nwith values:") - pprint(best_result['params'], indent=4) - if 'roi_t1' in best_result['params']: + pprint(params, indent=4) + if self.has_space('roi'): print("ROI table:") - pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4) + pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) def log_results(self, results) -> None: """ @@ -162,9 +169,7 @@ class Hyperopt(Backtesting): """ Tell if a space value is contained in the configuration """ - if space in self.config['spaces'] or 'all' in self.config['spaces']: - return True - return False + return any(s in self.config['spaces'] for s in [space, 'all']) def hyperopt_space(self) -> List[Dimension]: """ @@ -172,16 +177,16 @@ class Hyperopt(Backtesting): """ spaces: List[Dimension] = [] if self.has_space('buy'): + logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() if self.has_space('sell'): + logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() - # Make sure experimental is enabled - if 'experimental' not in self.config: - self.config['experimental'] = {} - self.config['experimental']['use_sell_signal'] = True if self.has_space('roi'): + logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() if self.has_space('stoploss'): + logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() return spaces From 3ccfe88ad86d57bd3c16dab2c18b4ea2cd61db81 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 1 Aug 2019 23:57:50 +0300 Subject: [PATCH 034/269] tests adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 5d6128e7c..e114dceaf 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -26,6 +26,7 @@ from freqtrade.tests.conftest import (get_args, log_has, log_has_re, @pytest.fixture(scope='function') def hyperopt(default_conf, mocker): + default_conf.update({'spaces': ['all']}) patch_exchange(mocker) return Hyperopt(default_conf) @@ -455,6 +456,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: hyperopt = Hyperopt(default_conf) hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() From 0413598d7bdd655e4d0d8ac88351d0b3b7aa5cf9 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Tue, 30 Jul 2019 21:04:05 -0400 Subject: [PATCH 035/269] adding environment.yml for conda builds --- environment.yml | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 environment.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 000000000..8f43b6991 --- /dev/null +++ b/environment.yml @@ -0,0 +1,58 @@ +name: freqtrade +channels: + - defaults + - conda-forge +dependencies: + # Required for app + - python>=3.6 + - pip + - wheel + - numpy + - pandas + - scipy + - SQLAlchemy + - scikit-learn + - arrow + - requests + - urllib3 + - wrapt + - joblib + - jsonschema + - tabulate + - python-rapidjson + - filelock + - flask + - python-dotenv + - cachetools + - scikit-optimize + - python-telegram-bot + # Optional for plotting + - plotly + # Optional for development + - flake8 + - pytest + - pytest-mock + - pytest-asyncio + - pytest-cov + - coveralls + - mypy + # Useful for jupyter + - ipykernel + - isort + - yapf + - pip: + # Required for app + - cython + - coinmarketcap + - ccxt + - TA-Lib + - py_find_1st + - sdnotify + # Optional for develpment + - flake8-tidy-imports + - flake8-type-annotations + - pytest-random-order + - -e . + + + From fceb41115448b4e7c776412094491ffce32926ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Aug 2019 20:08:30 +0200 Subject: [PATCH 036/269] Create detailed section about strategy problem analysis --- docs/data-analysis.md | 119 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 1940fa3e6..76fedf475 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -6,6 +6,125 @@ A good way for this is using Jupyter (notebook or lab) - which provides an inter The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results. +## Strategy development problem analysis + +Debugging a strategy (are there no buy signals, ...) can be very time-consuming. +FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. + +I recommend using Juptyer Notebooks for this analysis, since it offers a dynamic way to rerun certain parts. + +The following is a full code-snippet, which will be explained by both comments, and step by step below. + +```python +# Some necessary imports +from pathlib import Path + +from freqtrade.data.history import load_pair_history +from freqtrade.resolvers import StrategyResolver +# Define some constants +ticker_interval = "5m" + +# Name of the strategy class +strategyname = 'Awesomestrategy' +# Location of the strategy +strategy_location = '../xmatt/strategies' +# Location of the data +data_location = '../freqtrade/user_data/data/binance/' +# Only use one pair here +pair = "XRP_ETH" + +# Load data +bt_data = load_pair_history(datadir=Path(data_location), + ticker_interval = ticker_interval, + pair=pair) +print(len(bt_data)) + + +# Load strategy - best done in a new cell +# Needs to be ran each time the strategy-file is changed. +strategy = StrategyResolver({'strategy': strategyname, + 'user_data_dir': Path.cwd(), + 'strategy_path': location}).strategy + +# Run strategy (just like in backtesting) +df = strategy.analyze_ticker(bt_data, {'pair': pair}) +print(f"Generated {df['buy'].sum()} buy signals") + +# Reindex data to be "nicer" and show data +data = df.set_index('date', drop=True) +data.tail() + +``` + +### Explanation + +#### Imports and constant definition + +``` python +# Some necessary imports +from pathlib import Path + +from freqtrade.data.history import load_pair_history +from freqtrade.resolvers import StrategyResolver +# Define some constants +ticker_interval = "5m" + +# Name of the strategy class +strategyname = 'Awesomestrategy' +# Location of the strategy +strategy_location = 'user_data/strategies' +# Location of the data +data_location = 'user_data/data/binance' +# Only use one pair here +pair = "XRP_ETH" +``` + +This first section imports necessary modules, and defines some constants you'll probably need differently + +#### Load candles + +``` python +# Load data +bt_data = load_pair_history(datadir=Path(data_location), + ticker_interval = ticker_interval, + pair=pair) +print(len(bt_data)) +``` + +This second section loads the historic data and prints the amount of candles in the data. + +#### Run strategy and analyze results + +Now, it's time to load and run your strategy. +For this, I recommend using a new cell in your notebook, since you'll want to repeat this until you're satisfied with your strategy. + +``` python +# Load strategy - best done in a new cell +# Needs to be ran each time the strategy-file is changed. +strategy = StrategyResolver({'strategy': strategyname, + 'user_data_dir': Path.cwd(), + 'strategy_path': location}).strategy + +# Run strategy (just like in backtesting) +df = strategy.analyze_ticker(bt_data, {'pair': pair}) +print(f"Generated {df['buy'].sum()} buy signals") + +# Reindex data to be "nicer" and show data +data = df.set_index('date', drop=True) +data.tail() +``` + +The code snippet loads and analyzes the strategy, prints the number of buy signals. + +The last 2 lines serve to analyze the dataframe in detail. +This can be important if your strategy did not generate any buy signals. +Note that using `data.head()` would also work, however this is misleading since most indicators have some "startup" time at the start of a backtested dataframe. + +There can be many things wrong, some signs to look for are: + +* Columns with NaN values at the end of the dataframe +* Columns used in `crossed*()` functions with completely different units + ## Backtesting To analyze your backtest results, you can [export the trades](#exporting-trades-to-file). From 01cd30984b1e166ab59b446521350b74fa83eb12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 06:47:03 +0200 Subject: [PATCH 037/269] Improve wording --- docs/data-analysis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 76fedf475..2c5cc8842 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -79,7 +79,7 @@ data_location = 'user_data/data/binance' pair = "XRP_ETH" ``` -This first section imports necessary modules, and defines some constants you'll probably need differently +This first section imports necessary modules, and defines some constants you'll probably need to adjust for your case. #### Load candles From 76d22bc743f5e697b87e4991e6233c2a99b5e057 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 09:37:59 +0200 Subject: [PATCH 038/269] Show correct valueerror message --- freqtrade/data/btanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f2356c34b..5865d56a7 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -30,7 +30,7 @@ def load_backtest_data(filename) -> pd.DataFrame: filename = Path(filename) if not filename.is_file(): - raise ValueError("File {filename} does not exist.") + raise ValueError(f"File {filename} does not exist.") with filename.open() as file: data = json_load(file) From 0b9b5f39936967ad1e014d06032df14c18d6d31f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 19:50:12 +0200 Subject: [PATCH 039/269] Improve document wording --- docs/data-analysis.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 2c5cc8842..68e085ff3 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -11,7 +11,7 @@ The following helpers will help you loading the data into Pandas DataFrames, and Debugging a strategy (are there no buy signals, ...) can be very time-consuming. FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. -I recommend using Juptyer Notebooks for this analysis, since it offers a dynamic way to rerun certain parts. +It's recommendet using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. The following is a full code-snippet, which will be explained by both comments, and step by step below. @@ -33,6 +33,8 @@ data_location = '../freqtrade/user_data/data/binance/' # Only use one pair here pair = "XRP_ETH" +### End constants + # Load data bt_data = load_pair_history(datadir=Path(data_location), ticker_interval = ticker_interval, @@ -91,7 +93,8 @@ bt_data = load_pair_history(datadir=Path(data_location), print(len(bt_data)) ``` -This second section loads the historic data and prints the amount of candles in the data. +This second section loads the historic data and prints the amount of candles in the DataFrame. +You can also inspect this dataframe by using `bt_data.head()` or `bt_data.tail()`. #### Run strategy and analyze results @@ -114,7 +117,7 @@ data = df.set_index('date', drop=True) data.tail() ``` -The code snippet loads and analyzes the strategy, prints the number of buy signals. +The code snippet loads and analyzes the strategy, calculates and prints the number of buy signals. The last 2 lines serve to analyze the dataframe in detail. This can be important if your strategy did not generate any buy signals. From 32605fa10aea008c5332c7369d10df77e568db64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 19:52:56 +0200 Subject: [PATCH 040/269] small improvements --- docs/data-analysis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 68e085ff3..5099e1013 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -41,9 +41,9 @@ bt_data = load_pair_history(datadir=Path(data_location), pair=pair) print(len(bt_data)) - +### Start strategy reload # Load strategy - best done in a new cell -# Needs to be ran each time the strategy-file is changed. +# Rerun each time the strategy-file is changed. strategy = StrategyResolver({'strategy': strategyname, 'user_data_dir': Path.cwd(), 'strategy_path': location}).strategy From 3eb571f34c1ae12a33dd2dfc6f2f7a85f398898a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 20:04:18 +0200 Subject: [PATCH 041/269] recommended ... --- docs/data-analysis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 5099e1013..2b6d6ed58 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -11,7 +11,7 @@ The following helpers will help you loading the data into Pandas DataFrames, and Debugging a strategy (are there no buy signals, ...) can be very time-consuming. FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. -It's recommendet using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. +It's recommended using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. The following is a full code-snippet, which will be explained by both comments, and step by step below. From aa8f44f68c428962485231628a38dc9820009280 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 2 Aug 2019 22:22:58 +0300 Subject: [PATCH 042/269] improvements to hyperopt output --- freqtrade/optimize/hyperopt.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 479f8c70a..b68b1ac4f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -11,7 +11,7 @@ import sys from operator import itemgetter from pathlib import Path from pprint import pprint -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame @@ -70,7 +70,7 @@ class Hyperopt(Backtesting): if hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore - # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set + # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): self.max_open_trades = self.config['max_open_trades'] else: @@ -134,10 +134,19 @@ class Hyperopt(Backtesting): log_str = self.format_results_logstring(best_result) print(f"\nBest result:\n{log_str}\nwith values:") - pprint(params, indent=4) + if self.has_space('buy'): + print('Buy hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, + indent=4) + if self.has_space('sell'): + print('Sell hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, + indent=4) if self.has_space('roi'): print("ROI table:") pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) + if self.has_space('stoploss'): + print(f"Stoploss: {params.get('stoploss')}") def log_results(self, results) -> None: """ @@ -171,21 +180,24 @@ class Hyperopt(Backtesting): """ return any(s in self.config['spaces'] for s in [space, 'all']) - def hyperopt_space(self) -> List[Dimension]: + def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ - Return the space to use during Hyperopt + Return the dimentions in the hyperoptimization space. + :param space: Defines hyperspace to return dimentions for. + If None, then the self.has_space() will be used to return dimentions + for all hyperspaces used. """ spaces: List[Dimension] = [] - if self.has_space('buy'): + if space == 'buy' or (space is None and self.has_space('buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() - if self.has_space('sell'): + if space == 'sell' or (space is None and self.has_space('sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() - if self.has_space('roi'): + if space == 'roi' or (space is None and self.has_space('roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() - if self.has_space('stoploss'): + if space == 'stoploss' or (space is None and self.has_space('stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() return spaces From b152d1a7abd09774a6b3367b28a89bfeae4e4a47 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 2 Aug 2019 22:23:48 +0300 Subject: [PATCH 043/269] docs agjusted, plus minor fixes --- docs/hyperopt.md | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 2755cae2d..fbd00bcbd 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -303,8 +303,9 @@ Given the following result from hyperopt: ``` Best result: - 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 with values: +Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, @@ -347,21 +348,14 @@ If you are optimizing ROI, you're result will look as follows and include a ROI ``` Best result: - 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 with values: +Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, - 'adx-enabled': false, + 'adx-enabled': False, 'rsi-enabled': True, - 'trigger': 'bb_lower', - 'roi_t1': 40, - 'roi_t2': 57, - 'roi_t3': 21, - 'roi_p1': 0.03634636907306948, - 'roi_p2': 0.055237357937802885, - 'roi_p3': 0.015163796015548354, - 'stoploss': -0.37996664668703606 -} + 'trigger': 'bb_lower'} ROI table: { 0: 0.10674752302642071, 21: 0.09158372701087236, @@ -372,9 +366,9 @@ ROI table: This would translate to the following ROI table: ``` python - minimal_roi = { +minimal_roi = { "118": 0, - "78": 0.0363463, + "78": 0.0363, "21": 0.0915, "0": 0.106 } From cad7d9135a3c2a926993d43710dfb5fe2585858f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 09:24:27 +0300 Subject: [PATCH 044/269] tests: hide deprecation warning due to use of --live --- freqtrade/tests/optimize/test_backtesting.py | 3 +++ freqtrade/tests/test_configuration.py | 1 + 2 files changed, 4 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 9304871a8..37757743e 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -202,6 +202,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( @@ -812,6 +813,7 @@ def test_backtest_record(default_conf, fee, mocker): assert dur > 0 +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_live(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] @@ -858,6 +860,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): assert log_has(line, caplog.record_tuples) +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_multi_strat(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 56ff79625..1e76297a6 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -325,6 +325,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'export' not in config +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( From 3b65c986eea10e68da391997831a4d1a769aff2c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 10:20:20 +0300 Subject: [PATCH 045/269] wordings fixed --- freqtrade/optimize/hyperopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b68b1ac4f..8767ed9a7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -182,9 +182,9 @@ class Hyperopt(Backtesting): def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ - Return the dimentions in the hyperoptimization space. - :param space: Defines hyperspace to return dimentions for. - If None, then the self.has_space() will be used to return dimentions + Return the dimensions in the hyperoptimization space. + :param space: Defines hyperspace to return dimensions for. + If None, then the self.has_space() will be used to return dimensions for all hyperspaces used. """ spaces: List[Dimension] = [] From 13620df717bf73e38e3ac974dfca8d31cdd53178 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 11:05:05 +0300 Subject: [PATCH 046/269] 'with values:' line removed --- docs/hyperopt.md | 6 ++++-- freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index fbd00bcbd..0b5d1a50e 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -303,8 +303,9 @@ Given the following result from hyperopt: ``` Best result: + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -with values: + Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, @@ -348,8 +349,9 @@ If you are optimizing ROI, you're result will look as follows and include a ROI ``` Best result: + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -with values: + Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8767ed9a7..427b17cb8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -133,7 +133,7 @@ class Hyperopt(Backtesting): params = best_result['params'] log_str = self.format_results_logstring(best_result) - print(f"\nBest result:\n{log_str}\nwith values:") + print(f"\nBest result:\n\n{log_str}\n") if self.has_space('buy'): print('Buy hyperspace params:') pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, From e8b2ae0b85c1e311a87c037b3852fadb60d60dff Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 11:19:36 +0300 Subject: [PATCH 047/269] tests adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index e114dceaf..e3b049c06 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -463,7 +463,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: parallel.assert_called_once() out, err = capsys.readouterr() - assert 'Best result:\n* 1/1: foo result Objective: 1.00000\nwith values:\n' in out + assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called # Should be called twice, once for tickerdata, once to save evaluations assert dumper.call_count == 2 From bbd58e772eaa003aa18ce28c5fe2a15818f5d76f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 13:11:51 +0200 Subject: [PATCH 048/269] Warn when using restricted pairs As noted in https://github.com/ccxt/ccxt/issues/5624, there is currently no way to detect if a user is impacted by this or not prior to creating a order. --- freqtrade/exchange/exchange.py | 8 +++++++- freqtrade/tests/conftest.py | 18 +++++++++--------- freqtrade/tests/exchange/test_exchange.py | 21 +++++++++++++++++++-- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a7c76e635..37bbb778e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -260,7 +260,7 @@ class Exchange(object): if not self.markets: logger.warning('Unable to validate pairs (assuming they are correct).') - # return + return for pair in pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs @@ -269,6 +269,12 @@ class Exchange(object): raise OperationalException( f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') + elif self.markets[pair].get('info', {}).get('IsRestricted', False): + # Warn users about restricted pairs in whitelist. + # We cannot determine reliably if Users are affected. + logger.warning(f"Pair {pair} is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove {pair} from your whitelist.") def get_valid_pair_combination(self, curr_1, curr_2) -> str: """ diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 5862a2e89..71ed23901 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -304,7 +304,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'TKN/BTC': { 'id': 'tknbtc', @@ -329,7 +329,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'BLK/BTC': { 'id': 'blkbtc', @@ -354,7 +354,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'LTC/BTC': { 'id': 'ltcbtc', @@ -379,7 +379,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'XRP/BTC': { 'id': 'xrpbtc', @@ -404,7 +404,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'NEO/BTC': { 'id': 'neobtc', @@ -429,7 +429,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'BTT/BTC': { 'id': 'BTTBTC', @@ -457,7 +457,7 @@ def markets(): 'max': None } }, - 'info': "", + 'info': {}, }, 'ETH/USDT': { 'id': 'USDT-ETH', @@ -479,7 +479,7 @@ def markets(): } }, 'active': True, - 'info': "" + 'info': {}, }, 'LTC/USDT': { 'id': 'USDT-LTC', @@ -501,7 +501,7 @@ def markets(): 'max': None } }, - 'info': "" + 'info': {}, } } diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index a5cdf0a82..67a4bbeeb 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -318,7 +318,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog): def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + 'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {}, 'NEO/BTC': {} }) id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -332,7 +332,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'XRP/BTC': 'inactive' + 'XRP/BTC': {'inactive'} }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -361,6 +361,23 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.record_tuples) +def test_validate_pairs_restricted(default_conf, mocker, caplog): + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {}, + 'XRP/BTC': {'info': {'IsRestricted': True}} + }) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + + Exchange(default_conf) + assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove XRP/BTC from your whitelist.", + caplog.record_tuples) + + def test_validate_timeframes(default_conf, mocker): default_conf["ticker_interval"] = "5m" api_mock = MagicMock() From ad55faafa899fefc828bddd2e08a7f4a4f52749d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 13:18:37 +0200 Subject: [PATCH 049/269] Fix odd test --- freqtrade/tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 67a4bbeeb..2f9e525dd 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -332,7 +332,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'XRP/BTC': {'inactive'} + 'XRP/BTC': {'inactive': True} }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) From 8ab07e0451d1205eb4ebee2f2043f9df56cc2e13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 13:22:44 +0200 Subject: [PATCH 050/269] Add FAQ section about restricted markets --- docs/faq.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 83576af4d..fe6f66130 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -45,6 +45,16 @@ the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-c You can use the `/forcesell all` command from Telegram. +### I get the message "RESTRICTED_MARKET" + +Currently known to happen for US Bittrex users. +Bittrex split it's exchange into an US and an International verssion. +The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction. + +If you have restricted pairs in your whitelist, you'll get a Warning-message on startup for each restricted pair. +If you're an "International" Customer on the Bittrex exchange, then this warning will not impact you. +If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist. + ## Hyperopt module ### How many epoch do I need to get a good Hyperopt result? From d59608f7649ba5fc352c7a12d89b9466cbd11b2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 17:19:37 +0200 Subject: [PATCH 051/269] adjust some documentation wordings --- docs/faq.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index fe6f66130..e8367608f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -48,11 +48,11 @@ You can use the `/forcesell all` command from Telegram. ### I get the message "RESTRICTED_MARKET" Currently known to happen for US Bittrex users. -Bittrex split it's exchange into an US and an International verssion. +Bittrex split it's exchange into US and International versions. The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction. -If you have restricted pairs in your whitelist, you'll get a Warning-message on startup for each restricted pair. -If you're an "International" Customer on the Bittrex exchange, then this warning will not impact you. +If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair. +If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you. If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist. ## Hyperopt module From f200f52a163b67348a1e41c4cb5026e59c21d5d2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 19:09:42 +0300 Subject: [PATCH 052/269] hyperopt print colorized results --- docs/hyperopt.md | 4 ++++ freqtrade/configuration/arguments.py | 3 ++- freqtrade/configuration/cli_options.py | 6 ++++++ freqtrade/configuration/configuration.py | 3 +++ freqtrade/misc.py | 12 ++++++++++++ freqtrade/optimize/hyperopt.py | 18 ++++++++++++++++-- 6 files changed, 43 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0b5d1a50e..2826dfc3b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -343,6 +343,10 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: return dataframe ``` +You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. + +When the `--color/--print-colorized` command line option is used, the results are colorized -- bad results (with zero trades or limited by the `--min-trades` option) are red, currest bests -- in green, results with positive total profit are printed in bold. + ### Understand Hyperopt ROI results If you are optimizing ROI, you're result will look as follows and include a ROI table. diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 4f0c3d31b..dd5a4290e 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -23,7 +23,8 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "position_stacking", "epochs", "spaces", - "use_max_market_positions", "print_all", "hyperopt_jobs", + "use_max_market_positions", "print_all", + "print_colorized", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", "hyperopt_continue", "hyperopt_loss"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 04fde2051..aab3c12b3 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -191,6 +191,12 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', default=False, ), + "print_colorized": Arg( + '--color', '--print-colorized', + help='Print colorized hyperopt results.', + action='store_true', + default=False + ), "hyperopt_jobs": Arg( '-j', '--job-workers', help='The number of concurrently running jobs for hyperoptimization ' diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 17ad37d6a..01732ca9e 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -254,6 +254,9 @@ class Configuration(object): self._args_to_config(config, argname='print_all', logstring='Parameter --print-all detected ...') + self._args_to_config(config, argname='print_colorized', + logstring='Parameter --color/--print-colorized detected ...') + self._args_to_config(config, argname='hyperopt_jobs', logstring='Parameter -j/--job-workers detected: {}') diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 05946e008..eadfc7490 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -113,3 +113,15 @@ def deep_merge_dicts(source, destination): destination[key] = value return destination + + +def green(s): + return '\033[92m' + s + '\033[0m' + + +def red(s): + return '\033[91m' + s + '\033[0m' + + +def bold(s): + return '\033[1m' + s + '\033[0m' diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 427b17cb8..e62cbe66b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -20,6 +20,7 @@ from skopt.space import Dimension from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe +from freqtrade.misc import green, red, bold from freqtrade.optimize.backtesting import Backtesting # Import IHyperOptLoss to allow users import from this file from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 @@ -153,8 +154,19 @@ class Hyperopt(Backtesting): Log results if it is better than any previous evaluation """ print_all = self.config.get('print_all', False) - if print_all or results['loss'] < self.current_best_loss: + is_best_loss = results['loss'] < self.current_best_loss + if print_all or is_best_loss: + if is_best_loss: + self.current_best_loss = results['loss'] log_str = self.format_results_logstring(results) + # Colorize output + if self.config.get('print_colorized', False): + if results['total_profit'] > 0: + log_str = bold(log_str) + if results['loss'] >= MAX_LOSS: + log_str = red(log_str) + elif is_best_loss: + log_str = green(log_str) if print_all: print(log_str) else: @@ -169,7 +181,6 @@ class Hyperopt(Backtesting): total = self.total_epochs res = results['results_explanation'] loss = results['loss'] - self.current_best_loss = results['loss'] log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}' return log_str @@ -237,6 +248,7 @@ class Hyperopt(Backtesting): results_explanation = self.format_results(results) trade_count = len(results.index) + total_profit = results.profit_abs.sum() # If this evaluation contains too short amount of trades to be # interesting -- consider it as 'bad' (assigned max. loss value) @@ -247,6 +259,7 @@ class Hyperopt(Backtesting): 'loss': MAX_LOSS, 'params': params, 'results_explanation': results_explanation, + 'total_profit': total_profit, } loss = self.calculate_loss(results=results, trade_count=trade_count, @@ -256,6 +269,7 @@ class Hyperopt(Backtesting): 'loss': loss, 'params': params, 'results_explanation': results_explanation, + 'total_profit': total_profit, } def format_results(self, results: DataFrame) -> str: From fe796c46c329d51c36761ef74ec24a53fc4dbf3e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 19:13:18 +0300 Subject: [PATCH 053/269] test adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index e3b049c06..4fb08181a 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -605,7 +605,8 @@ def test_generate_optimizer(mocker, default_conf) -> None: 'loss': 1.9840569076926293, 'results_explanation': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' '( 2.31Σ%). Avg duration 100.0 mins.', - 'params': optimizer_param + 'params': optimizer_param, + 'total_profit': 0.00023300 } hyperopt = Hyperopt(default_conf) From 3dd6fe2703a4884eaa43443980e523465b381435 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 19:44:32 +0300 Subject: [PATCH 054/269] wording --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 2826dfc3b..16e2212d5 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -345,7 +345,7 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. -When the `--color/--print-colorized` command line option is used, the results are colorized -- bad results (with zero trades or limited by the `--min-trades` option) are red, currest bests -- in green, results with positive total profit are printed in bold. +When the `--color/--print-colorized` command line option is used, the results are colorized -- bad results (with zero trades or limited by the `--min-trades` option) are red, current bests -- in green, results with positive total profit are printed in bold. ### Understand Hyperopt ROI results From c4e30862eeda84a1aa7f4e587c6851bf05905b4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 19:55:54 +0200 Subject: [PATCH 055/269] load_trades_db should give as many columns as possible --- freqtrade/data/btanalysis.py | 21 ++++++++++++++++----- freqtrade/tests/data/test_btanalysis.py | 5 +++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 5865d56a7..6568fe31e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -81,19 +81,30 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: """ trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) persistence.init(db_url, clean_open_orders=False) - columns = ["pair", "profit", "open_time", "close_time", - "open_rate", "close_rate", "duration", "sell_reason", - "max_rate", "min_rate"] - trades = pd.DataFrame([(t.pair, t.calc_profit(), + columns = ["pair", "open_time", "close_time", "profit", "profitperc", + "open_rate", "close_rate", "amount", "duration", "sell_reason", + "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", + "stake_amount", "max_rate", "min_rate", "id", "exchange", + "stop_loss", "initial_stop_loss", "strategy", "ticker_interval"] + + trades = pd.DataFrame([(t.pair, t.open_date.replace(tzinfo=pytz.UTC), t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, - t.open_rate, t.close_rate, + t.calc_profit(), t.calc_profit_percent(), + t.open_rate, t.close_rate, t.amount, t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None, t.sell_reason, + t.fee_open, t.fee_close, + t.open_rate_requested, + t.close_rate_requested, + t.stake_amount, t.max_rate, t.min_rate, + t.id, t.exchange, + t.stop_loss, t.initial_stop_loss, + t.strategy, t.ticker_interval ) for t in Trade.query.all()], columns=columns) diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index e80840009..bad8db66f 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -45,6 +45,11 @@ def test_load_trades_db(default_conf, fee, mocker): assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_time" in trades.columns + assert "profitperc" in trades.columns + + for col in BT_DATA_COLUMNS: + if col not in ['index', 'open_at_end']: + assert col in trades.columns def test_extract_trades_of_period(): From d51fd1a5d0bec672fcadd92fcc38cf15add751ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 19:56:41 +0200 Subject: [PATCH 056/269] fix typo --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index e8367608f..a441ffacd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -48,7 +48,7 @@ You can use the `/forcesell all` command from Telegram. ### I get the message "RESTRICTED_MARKET" Currently known to happen for US Bittrex users. -Bittrex split it's exchange into US and International versions. +Bittrex split its exchange into US and International versions. The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction. If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair. From c6bd14378502aeccf4ee2b6af120ca90922e61be Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 20:04:49 +0200 Subject: [PATCH 057/269] add Operating system to issue template --- .github/ISSUE_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 94d998310..ae5375f43 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,6 +5,7 @@ If it hasn't been reported, please create a new issue. ## Step 2: Describe your environment + * Operating system: ____ * Python Version: _____ (`python -V`) * CCXT version: _____ (`pip freeze | grep ccxt`) * Branch: Master | Develop From 52d92cba90cd3e9baa1c48da8e47c8ddb68a4fb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:20:31 +0200 Subject: [PATCH 058/269] Split analyze_ticker and _analyze_ticker_int --- freqtrade/strategy/interface.py | 22 +++++++++++++++++----- freqtrade/tests/strategy/test_interface.py | 20 ++++++++++---------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2a28bcd22..5bd183fa9 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -158,6 +158,21 @@ class IStrategy(ABC): """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it + :param dataframe: Dataframe containing ticker data + :param metadata: Metadata dictionary with additional data (e.g. 'pair') + :return: DataFrame with ticker data and indicator data + """ + logger.debug("TA Analysis Launched") + dataframe = self.advise_indicators(dataframe, metadata) + dataframe = self.advise_buy(dataframe, metadata) + dataframe = self.advise_sell(dataframe, metadata) + return dataframe + + def _analyze_ticker_int(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Parses the given ticker history and returns a populated DataFrame + add several TA indicators and buy signal to it + Used internally, may skip analysis if `process_only_new_candles` is set. :return: DataFrame with ticker data and indicator data """ @@ -168,10 +183,7 @@ class IStrategy(ABC): if (not self.process_only_new_candles or self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']): # Defs that only make change on new candle data. - logger.debug("TA Analysis Launched") - dataframe = self.advise_indicators(dataframe, metadata) - dataframe = self.advise_buy(dataframe, metadata) - dataframe = self.advise_sell(dataframe, metadata) + dataframe = self.analyze_ticker(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] else: logger.debug("Skipping TA Analysis for already analyzed candle") @@ -198,7 +210,7 @@ class IStrategy(ABC): return False, False try: - dataframe = self.analyze_ticker(dataframe, {'pair': pair}) + dataframe = self._analyze_ticker_int(dataframe, {'pair': pair}) except ValueError as error: logger.warning( 'Unable to analyze ticker for pair %s: %s', diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index ee8c8ddd4..6bf3770c5 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -19,13 +19,13 @@ _STRATEGY = DefaultStrategy(config={}) def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) @@ -33,14 +33,14 @@ def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): def test_returns_latest_sell_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) @@ -60,7 +60,7 @@ def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', side_effect=ValueError('xyz') ) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], @@ -71,7 +71,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_hi def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([]) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -86,7 +86,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): oldtime = arrow.utcnow().shift(minutes=-16) ticks = DataFrame([{'buy': 1, 'date': oldtime}]) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame(ticks) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -252,7 +252,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: caplog.record_tuples) -def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: +def test__analyze_ticker_int_skip_analyze(ticker_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x) @@ -267,7 +267,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: strategy = DefaultStrategy({}) strategy.process_only_new_candles = True - ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_int(ticker_history, {'pair': 'ETH/BTC'}) assert 'high' in ret.columns assert 'low' in ret.columns assert 'close' in ret.columns @@ -280,7 +280,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: caplog.record_tuples) caplog.clear() - ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_int(ticker_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 From 62262d0bb50a398c09e4d9f31f5f6ab9176f972c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:21:22 +0200 Subject: [PATCH 059/269] improve docstring of _analyze_ticker_int --- freqtrade/strategy/interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5bd183fa9..48a4f96a2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -172,7 +172,9 @@ class IStrategy(ABC): """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it - Used internally, may skip analysis if `process_only_new_candles` is set. + WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set. + :param dataframe: Dataframe containing ticker data + :param metadata: Metadata dictionary with additional data (e.g. 'pair') :return: DataFrame with ticker data and indicator data """ From e4380b533bc4515cd6fc227690d7efb77b945d12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:25:46 +0200 Subject: [PATCH 060/269] Print plot filename so it can be easily opened --- freqtrade/plot/plotting.py | 5 +++-- freqtrade/tests/test_plotting.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 5c9c6e457..d03d3ae53 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -318,6 +318,7 @@ def store_plot_file(fig, filename: str, auto_open: bool = False) -> None: """ Path("user_data/plots").mkdir(parents=True, exist_ok=True) - - plot(fig, filename=str(Path('user_data/plots').joinpath(filename)), + _filename = Path('user_data/plots').joinpath(filename) + plot(fig, filename=str(_filename), auto_open=auto_open) + logger.info(f"Stored plot as {_filename}") diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 509bf7880..f9da773fe 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -215,6 +215,8 @@ def test_generate_plot_file(mocker, caplog): assert plot_mock.call_args[0][0] == fig assert (plot_mock.call_args_list[0][1]['filename'] == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") + assert log_has("Stored plot as user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html", + caplog.record_tuples) def test_add_profit(): From c5ccf44750d70cc670671ea50af19d710468b97e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:26:04 +0200 Subject: [PATCH 061/269] Remove generate_dataframe from plot_dataframe script --- scripts/plot_dataframe.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 034a6f448..6412c45a4 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -16,8 +16,6 @@ import logging import sys from typing import Any, Dict, List -import pandas as pd - from freqtrade.configuration import Arguments from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME from freqtrade.data.btanalysis import extract_trades_of_period @@ -30,20 +28,6 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: - """ - Get tickers then Populate strategy indicators and signals, then return the full dataframe - :return: the DataFrame of a pair - """ - - dataframes = strategy.tickerdata_to_dataframe(tickers) - dataframe = dataframes[pair] - dataframe = strategy.advise_buy(dataframe, {'pair': pair}) - dataframe = strategy.advise_sell(dataframe, {'pair': pair}) - - return dataframe - - def analyse_and_plot_pairs(config: Dict[str, Any]): """ From arguments provided in cli: @@ -57,6 +41,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): """ plot_elements = init_plotscript(config) trades = plot_elements['trades'] + strategy = plot_elements["strategy"] pair_counter = 0 for pair, data in plot_elements["tickers"].items(): @@ -64,7 +49,8 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): logger.info("analyse pair %s", pair) tickers = {} tickers[pair] = data - dataframe = generate_dataframe(plot_elements["strategy"], tickers, pair) + + dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) From 4d1ce8178c8abc55488932f07b20055c08497312 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:38:37 +0200 Subject: [PATCH 062/269] intend if to be clearer --- freqtrade/data/btanalysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6568fe31e..36d8aedbb 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -93,8 +93,8 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, t.calc_profit(), t.calc_profit_percent(), t.open_rate, t.close_rate, t.amount, - t.close_date.timestamp() - t.open_date.timestamp() - if t.close_date else None, + (t.close_date.timestamp() - t.open_date.timestamp() + if t.close_date else None), t.sell_reason, t.fee_open, t.fee_close, t.open_rate_requested, From 2af663dccbd4fe395984125778729d5c968df087 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 12:55:03 +0200 Subject: [PATCH 063/269] rename _analyze_ticker_int to _analyze_ticker_internal --- freqtrade/strategy/interface.py | 4 ++-- freqtrade/tests/strategy/test_interface.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 48a4f96a2..37aa97bb1 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -168,7 +168,7 @@ class IStrategy(ABC): dataframe = self.advise_sell(dataframe, metadata) return dataframe - def _analyze_ticker_int(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it @@ -212,7 +212,7 @@ class IStrategy(ABC): return False, False try: - dataframe = self._analyze_ticker_int(dataframe, {'pair': pair}) + dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) except ValueError as error: logger.warning( 'Unable to analyze ticker for pair %s: %s', diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 6bf3770c5..9f5ab70e3 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -19,13 +19,13 @@ _STRATEGY = DefaultStrategy(config={}) def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) @@ -33,14 +33,14 @@ def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): def test_returns_latest_sell_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) @@ -60,7 +60,7 @@ def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', side_effect=ValueError('xyz') ) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], @@ -71,7 +71,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_hi def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([]) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -86,7 +86,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): oldtime = arrow.utcnow().shift(minutes=-16) ticks = DataFrame([{'buy': 1, 'date': oldtime}]) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame(ticks) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -252,7 +252,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: caplog.record_tuples) -def test__analyze_ticker_int_skip_analyze(ticker_history, mocker, caplog) -> None: +def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x) @@ -267,7 +267,7 @@ def test__analyze_ticker_int_skip_analyze(ticker_history, mocker, caplog) -> Non strategy = DefaultStrategy({}) strategy.process_only_new_candles = True - ret = strategy._analyze_ticker_int(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) assert 'high' in ret.columns assert 'low' in ret.columns assert 'close' in ret.columns @@ -280,7 +280,7 @@ def test__analyze_ticker_int_skip_analyze(ticker_history, mocker, caplog) -> Non caplog.record_tuples) caplog.clear() - ret = strategy._analyze_ticker_int(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 From 9cbab35de01db7338563d4a029d4c62d7cbd9fe7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 4 Aug 2019 22:54:19 +0300 Subject: [PATCH 064/269] colorization by means of termcolor and colorama --- freqtrade/misc.py | 12 ------------ freqtrade/optimize/hyperopt.py | 12 ++++++++---- requirements-common.txt | 4 ++++ 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index eadfc7490..05946e008 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -113,15 +113,3 @@ def deep_merge_dicts(source, destination): destination[key] = value return destination - - -def green(s): - return '\033[92m' + s + '\033[0m' - - -def red(s): - return '\033[91m' + s + '\033[0m' - - -def bold(s): - return '\033[1m' + s + '\033[0m' diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e62cbe66b..fc0fb56b2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -13,14 +13,15 @@ from pathlib import Path from pprint import pprint from typing import Any, Dict, List, Optional +from colorama import init as colorama_init from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension +from termcolor import colored from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe -from freqtrade.misc import green, red, bold from freqtrade.optimize.backtesting import Backtesting # Import IHyperOptLoss to allow users import from this file from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 @@ -162,11 +163,11 @@ class Hyperopt(Backtesting): # Colorize output if self.config.get('print_colorized', False): if results['total_profit'] > 0: - log_str = bold(log_str) + log_str = colored(log_str, attrs=['bold']) if results['loss'] >= MAX_LOSS: - log_str = red(log_str) + log_str = colored(log_str, 'red') elif is_best_loss: - log_str = green(log_str) + log_str = colored(log_str, 'green') if print_all: print(log_str) else: @@ -353,6 +354,9 @@ class Hyperopt(Backtesting): logger.info(f'Number of parallel jobs set as: {config_jobs}') opt = self.get_optimizer(config_jobs) + + colorama_init() + try: with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() diff --git a/requirements-common.txt b/requirements-common.txt index 2e52b84ad..b0c4c6729 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -30,3 +30,7 @@ sdnotify==0.3.2 # Api server flask==1.1.1 + +# Support for colorized terminal output +termcolor==1.1.0 +colorama==0.4.1 From c6444a10a8463530a75dfaa8d4a2f94fee849e38 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 5 Aug 2019 17:54:53 +0300 Subject: [PATCH 065/269] move roi_space, stoploss_space, generate_roi_table to IHyperOpt --- docs/hyperopt.md | 56 ++++++++++++++++++++---- freqtrade/optimize/default_hyperopt.py | 38 +--------------- freqtrade/optimize/hyperopt_interface.py | 52 ++++++++++++++++------ user_data/hyperopts/sample_hyperopt.py | 55 +++++------------------ 4 files changed, 99 insertions(+), 102 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0b5d1a50e..0304c3188 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -18,19 +18,24 @@ Configuring hyperopt is similar to writing your own strategy, and many tasks wil ### Checklist on all tasks / possibilities in hyperopt -Depending on the space you want to optimize, only some of the below are required. +Depending on the space you want to optimize, only some of the below are required: * fill `populate_indicators` - probably a copy from your strategy * fill `buy_strategy_generator` - for buy signal optimization * fill `indicator_space` - for buy signal optimzation * fill `sell_strategy_generator` - for sell signal optimization * fill `sell_indicator_space` - for sell signal optimzation -* fill `roi_space` - for ROI optimization -* fill `generate_roi_table` - for ROI optimization (if you need more than 3 entries) -* fill `stoploss_space` - stoploss optimization -* Optional but recommended - * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used - * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used + +Optional, but recommended: + +* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used +* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need more than 4 entries in the ROI table) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) ### 1. Install a Custom Hyperopt File @@ -345,7 +350,7 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: ### Understand Hyperopt ROI results -If you are optimizing ROI, you're result will look as follows and include a ROI table. +If you are optimizing ROI, your result will look as follows and include a ROI table: ``` Best result: @@ -376,6 +381,41 @@ minimal_roi = { } ``` +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps) with the values that can vary in the following ranges: + +| # | minutes | ROI percentage | +|---|---|---| +| 1 | always 0 | 0.03...0.31 | +| 2 | 10...40 | 0.02...0.11 | +| 3 | 20...100 | 0.01...0.04 | +| 4 | 30...220 | always 0 | + +This structure of the ROI table is sufficient in most cases. Override the `roi_space()` method defining the ranges desired 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 in these methods if you need a different structure of the ROI table or other amount of rows (steps) in the ROI tables. + +### Understand Hyperopt Stoploss results + +If you are optimizing stoploss values, your result will look as follows and include stoploss: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +Stoploss: -0.37996664668703606 +``` + +If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases. + +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. + ### Validate backtesting results Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index ad76ff786..1d4c40916 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, List import talib.abstract as ta from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -156,42 +156,6 @@ class DefaultHyperOpts(IHyperOpt): 'sell-sar_reversal'], name='sell-trigger') ] - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: - """ - Generate the ROI table that will be used by Hyperopt - """ - roi_table = {} - roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] - roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] - roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] - roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 - - return roi_table - - @staticmethod - def stoploss_space() -> List[Dimension]: - """ - Stoploss Value to search - """ - return [ - Real(-0.5, -0.02, name='stoploss'), - ] - - @staticmethod - def roi_space() -> List[Dimension]: - """ - Values to search for each ROI steps - """ - return [ - Integer(10, 120, name='roi_t1'), - Integer(10, 60, name='roi_t2'), - Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), - ] - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators. Should be a copy of from strategy diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 08823ece0..f1f123653 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from typing import Dict, Any, Callable, List from pandas import DataFrame -from skopt.space import Dimension +from skopt.space import Dimension, Integer, Real class IHyperOpt(ABC): @@ -26,56 +26,80 @@ class IHyperOpt(ABC): @abstractmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> 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 + 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. """ @staticmethod @abstractmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Create a buy strategy generator + Create a buy strategy generator. """ @staticmethod @abstractmethod def sell_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Create a sell strategy generator + Create a sell strategy generator. """ @staticmethod @abstractmethod def indicator_space() -> List[Dimension]: """ - Create an indicator space + Create an indicator space. """ @staticmethod @abstractmethod def sell_indicator_space() -> List[Dimension]: """ - Create a sell indicator space + Create a sell indicator space. """ @staticmethod - @abstractmethod def generate_roi_table(params: Dict) -> Dict[int, float]: """ - Create an roi table + Create a ROI table. + + Generates the ROI table that will be used by Hyperopt. + You may override it in your custom Hyperopt class. """ + roi_table = {} + roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] + roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] + roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] + roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 + + return roi_table @staticmethod - @abstractmethod def stoploss_space() -> List[Dimension]: """ - Create a stoploss space + Create a stoploss space. + + Defines range of stoploss values to search. + You may override it in your custom Hyperopt class. """ + return [ + Real(-0.5, -0.02, name='stoploss'), + ] @staticmethod - @abstractmethod def roi_space() -> List[Dimension]: """ - Create a roi space + Create a ROI space. + + Defines values to search for each ROI steps. + You may override it in your custom Hyperopt class. """ + return [ + Integer(10, 120, name='roi_t1'), + Integer(10, 60, name='roi_t2'), + Integer(10, 40, name='roi_t3'), + Real(0.01, 0.04, name='roi_p1'), + Real(0.01, 0.07, name='roi_p2'), + Real(0.01, 0.20, name='roi_p3'), + ] diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index a78906cf3..9a755e52e 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -18,14 +18,19 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt class SampleHyperOpts(IHyperOpt): """ This is a test hyperopt to inspire you. + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md - You can: - - Rename the class name (Do not forget to update class_name) - - Add any methods you want to build your hyperopt - - Add any lib you need to build your hyperopt - You must keep: - - the prototype for the methods: populate_indicators, indicator_space, buy_strategy_generator, - roi_space, generate_roi_table, stoploss_space + + You can: + - Rename the class name. + - Add any methods you want to build your hyperopt. + - 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 were moved to the parent class, you may + override them here if you need it. """ @staticmethod @@ -167,42 +172,6 @@ class SampleHyperOpts(IHyperOpt): 'sell-sar_reversal'], name='sell-trigger') ] - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: - """ - Generate the ROI table that will be used by Hyperopt - """ - roi_table = {} - roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] - roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] - roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] - roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 - - return roi_table - - @staticmethod - def stoploss_space() -> List[Dimension]: - """ - Stoploss Value to search - """ - return [ - Real(-0.5, -0.02, name='stoploss'), - ] - - @staticmethod - def roi_space() -> List[Dimension]: - """ - Values to search for each ROI steps - """ - return [ - Integer(10, 120, name='roi_t1'), - Integer(10, 60, name='roi_t2'), - Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), - ] - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators. Should be a copy of from strategy From 9914198a6c90bcd92b5702115c984ec66cb7faa5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:09 +0000 Subject: [PATCH 066/269] Update ccxt from 1.18.992 to 1.18.1008 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 2e52b84ad..dcc3be754 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.18.992 +ccxt==1.18.1008 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.3 From 187d029d204adf2403d02d57781a94772d6b2891 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:10 +0000 Subject: [PATCH 067/269] Update arrow from 0.14.3 to 0.14.4 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index dcc3be754..675a28d6a 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.1008 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 -arrow==0.14.3 +arrow==0.14.4 cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore From 930c25f7f113f1354b056eea772af36cdcf988cb Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:11 +0000 Subject: [PATCH 068/269] Update scikit-learn from 0.21.2 to 0.21.3 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 675a28d6a..f03a90927 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -8,7 +8,7 @@ cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.2 -scikit-learn==0.21.2 +scikit-learn==0.21.3 joblib==0.13.2 jsonschema==3.0.1 TA-Lib==0.4.17 From 403f7668d5e300db1c3a7f26d65a781be3a3dcaf Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:16 +0000 Subject: [PATCH 069/269] Update jsonschema from 3.0.1 to 3.0.2 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index f03a90927..b90c13cec 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -10,7 +10,7 @@ urllib3==1.24.2 # pyup: ignore wrapt==1.11.2 scikit-learn==0.21.3 joblib==0.13.2 -jsonschema==3.0.1 +jsonschema==3.0.2 TA-Lib==0.4.17 tabulate==0.8.3 coinmarketcap==5.0.3 From d71102c45ad89a62ec6423a53d6f092f628262cc Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:17 +0000 Subject: [PATCH 070/269] Update py_find_1st from 1.1.3 to 1.1.4 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b90c13cec..7280dfc6e 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -20,7 +20,7 @@ scikit-optimize==0.5.2 filelock==3.0.12 # find first, C search in arrays -py_find_1st==1.1.3 +py_find_1st==1.1.4 #Load ticker files 30% faster python-rapidjson==0.7.2 From 5e64d629a3c338ff9684c60bb7ec612c47a7a362 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:19 +0000 Subject: [PATCH 071/269] Update coveralls from 1.8.1 to 1.8.2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f54b38a57..03b37417e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ -r requirements.txt -r requirements-plot.txt -coveralls==1.8.1 +coveralls==1.8.2 flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 From 904381058c2394248238f2d9aed5c86c9f271ab1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 19:25:43 +0200 Subject: [PATCH 072/269] Add documentation for conda install --- docs/installation.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index 74b3a3202..589d3fe7f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -219,6 +219,17 @@ as the watchdog. ------ +## Using Conda + +Freqtrade can also be installed using Anaconda (or Miniconda). + +``` bash +conda env create -f environment.yml +``` + +!!! Note: + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. + ## Windows We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). From 3721610a6321f3ec423a4ccf3968b1b64214e260 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 20:06:42 +0200 Subject: [PATCH 073/269] Add new detailed trade-scenario tests covers cases raised in #1971 --- .../tests/optimize/test_backtest_detail.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 402e22391..bc58b5ae4 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -224,6 +224,53 @@ tc12 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] ) +# Test 13 - Buy and sell ROI on same candle +# Set stop-loss at 10% (should not apply), ROI at 1% +tc13 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 4850, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4850, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.01, profit_perc=0.01, trailing_stop=False, + trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] +) + +# Test 14 - Buy and Stoploss on same candle +# Set stop-loss at 5%, ROI at 10% (should not apply) +tc14 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4600, 5100, 6172, 0, 0], + [2, 5100, 5251, 4850, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.05, roi=0.10, profit_perc=-0.05, trailing_stop=False, + trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] +) + + +# Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle +# Set stop-loss at 5%, ROI at 10% (should not apply) +tc15 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4900, 5100, 6172, 1, 0], + [2, 5100, 5251, 4650, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.05, roi=0.01, profit_perc=-0.04, trailing_stop=False, + trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1), + BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] +) + TESTS = [ tc0, tc1, @@ -238,6 +285,9 @@ TESTS = [ tc10, tc11, tc12, + tc13, + tc14, + tc15, ] From bc2e920ae2bde9fc0483cc336402507a147dad84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 20:07:29 +0200 Subject: [PATCH 074/269] Adjust code to verify "current" candle for buy/sells --- freqtrade/optimize/backtesting.py | 4 +++- freqtrade/tests/optimize/test_backtesting.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 429633f31..dcccd36a3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -373,7 +373,9 @@ class Backtesting(object): continue trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:], + # since indexes has been incremented before, we need to go one step back to + # also check the buying candle for sell conditions. + trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:], trade_count_lock, stake_amount, max_open_trades) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 37757743e..59679d1a2 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -618,8 +618,9 @@ def test_processed(default_conf, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None: + # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [['raise', 19], ['lower', 0], ['sine', 18]] + tests = [['raise', 19], ['lower', 0], ['sine', 35]] # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} From c7d032975471072932ebfdaa169a10a2a7b4ec86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 20:19:19 +0200 Subject: [PATCH 075/269] Clean up comments of detail-backtests --- .../tests/optimize/test_backtest_detail.py | 86 ++++++++----------- 1 file changed, 34 insertions(+), 52 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index bc58b5ae4..87f567b4f 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -14,9 +14,8 @@ from freqtrade.tests.optimize import (BTContainer, BTrade, _get_frame_time_from_offset, tests_ticker_interval) -# Test 0 Sell signal sell +# Test 0: Sell with signal sell in candle 3 # Test with Stop-loss at 1% -# TC0: Sell signal in candle 3 tc0 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -29,9 +28,8 @@ tc0 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) -# Test 1 Minus 8% Close +# Test 1: Stop-Loss Triggered 1% loss # Test with Stop-loss at 1% -# TC1: Stop-Loss Triggered 1% loss tc1 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -45,9 +43,8 @@ tc1 = BTContainer(data=[ ) -# Test 2 Minus 4% Low, minus 1% close +# Test 2: Minus 4% Low, minus 1% close # Test with Stop-Loss at 3% -# TC2: Stop-Loss Triggered 3% Loss tc2 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -61,12 +58,12 @@ tc2 = BTContainer(data=[ ) -# Test 3 Candle drops 4%, Recovers 1%. -# Entry Criteria Met -# Candle drops 20% -# Test with Stop-Loss at 2% -# TC3: Trade-A: Stop-Loss Triggered 2% Loss -# Trade-B: Stop-Loss Triggered 2% Loss +# Test 3: Multiple trades. +# Candle drops 4%, Recovers 1%. +# Entry Criteria Met +# Candle drops 20% +# Trade-A: Stop-Loss Triggered 2% Loss +# Trade-B: Stop-Loss Triggered 2% Loss tc3 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -81,10 +78,10 @@ tc3 = BTContainer(data=[ BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)] ) -# Test 4 Minus 3% / recovery +15% +# Test 4: Minus 3% / recovery +15% # Candle Data for test 3 – Candle drops 3% Closed 15% up # Test with Stop-loss at 2% ROI 6% -# TC4: Stop-Loss Triggered 2% Loss +# Stop-Loss Triggered 2% Loss tc4 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -97,9 +94,8 @@ tc4 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 5 / Drops 0.5% Closes +20% -# Set stop-loss at 1% ROI 3% -# TC5: ROI triggers 3% Gain +# Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain +# stop-loss: 1%, ROI: 3% tc5 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4980, 4987, 6172, 1, 0], @@ -112,9 +108,8 @@ tc5 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) -# Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve -# Set stop-loss at 2% ROI at 5% -# TC6: Stop-Loss triggers 2% Loss +# Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss +# stop-loss: 2% ROI: 5% tc6 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -127,9 +122,8 @@ tc6 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 7 - 6% Positive / 1% Negative / Close 1% Positve -# Set stop-loss at 2% ROI at 3% -# TC7: ROI Triggers 3% Gain +# Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain +# stop-loss: 2% ROI: 3% tc7 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -143,9 +137,8 @@ tc7 = BTContainer(data=[ ) -# Test 8 - trailing_stop should raise so candle 3 causes a stoploss. -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC8: Trailing stoploss - stoploss should be adjusted candle 2 +# Test 8: trailing_stop should raise so candle 3 causes a stoploss. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 2 tc8 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -158,10 +151,8 @@ tc8 = BTContainer(data=[ ) -# Test 9 - trailing_stop should raise - high and low in same candle. -# Candle Data for test 9 -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC9: Trailing stoploss - stoploss should be adjusted candle 3 +# Test 9: trailing_stop should raise - high and low in same candle. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 3 tc9 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -173,10 +164,9 @@ tc9 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) -# Test 10 - trailing_stop should raise so candle 3 causes a stoploss +# Test 10: trailing_stop should raise so candle 3 causes a stoploss # without applying trailing_stop_positive since stoploss_offset is at 10%. -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC10: Trailing stoploss - stoploss should be adjusted candle 2 +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc10 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -190,10 +180,9 @@ tc10 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)] ) -# Test 11 - trailing_stop should raise so candle 3 causes a stoploss +# Test 11: trailing_stop should raise so candle 3 causes a stoploss # applying a positive trailing stop of 3% since stop_positive_offset is reached. -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC11: Trailing stoploss - stoploss should be adjusted candle 2, +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc11 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -207,10 +196,9 @@ tc11 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) -# Test 12 - trailing_stop should raise in candle 2 and cause a stoploss in the same candle +# Test 12: trailing_stop should raise in candle 2 and cause a stoploss in the same candle # applying a positive trailing stop of 3% since stop_positive_offset is reached. -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC12: Trailing stoploss - stoploss should be adjusted candle 2, +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc12 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -224,8 +212,8 @@ tc12 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 13 - Buy and sell ROI on same candle -# Set stop-loss at 10% (should not apply), ROI at 1% +# Test 13: Buy and sell ROI on same candle +# stop-loss: 10% (should not apply), ROI: 1% tc13 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -233,14 +221,12 @@ tc13 = BTContainer(data=[ [2, 5100, 5251, 4850, 5100, 6172, 0, 0], [3, 4850, 5050, 4850, 4750, 6172, 0, 0], [4, 4750, 4950, 4850, 4750, 6172, 0, 0]], - stop_loss=-0.10, roi=0.01, profit_perc=0.01, trailing_stop=False, - trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, - trailing_stop_positive=0.03, + stop_loss=-0.10, roi=0.01, profit_perc=0.01, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] ) # Test 14 - Buy and Stoploss on same candle -# Set stop-loss at 5%, ROI at 10% (should not apply) +# stop-loss: 5%, ROI: 10% (should not apply) tc14 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -248,15 +234,13 @@ tc14 = BTContainer(data=[ [2, 5100, 5251, 4850, 5100, 6172, 0, 0], [3, 4850, 5050, 4850, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], - stop_loss=-0.05, roi=0.10, profit_perc=-0.05, trailing_stop=False, - trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, - trailing_stop_positive=0.03, + stop_loss=-0.05, roi=0.10, profit_perc=-0.05, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] ) # Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle -# Set stop-loss at 5%, ROI at 10% (should not apply) +# stop-loss: 5%, ROI: 10% (should not apply) tc15 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -264,9 +248,7 @@ tc15 = BTContainer(data=[ [2, 5100, 5251, 4650, 5100, 6172, 0, 0], [3, 4850, 5050, 4850, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], - stop_loss=-0.05, roi=0.01, profit_perc=-0.04, trailing_stop=False, - trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, - trailing_stop_positive=0.03, + stop_loss=-0.05, roi=0.01, profit_perc=-0.04, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1), BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] ) From 0376630f7ad156ec631d9da7ee03fa078022a922 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 20:25:20 +0200 Subject: [PATCH 076/269] Update urllib to latest version --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 2e52b84ad..475031660 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -6,7 +6,7 @@ python-telegram-bot==11.1.0 arrow==0.14.3 cachetools==3.1.1 requests==2.22.0 -urllib3==1.24.2 # pyup: ignore +urllib3==1.25.3 wrapt==1.11.2 scikit-learn==0.21.2 joblib==0.13.2 From 988a0245c28ca402186cd99c6f428eafaa719026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2019 20:27:42 +0200 Subject: [PATCH 077/269] Update install-script to use parameter Use --prefix /usr/local for install-script too --- .travis.yml | 14 +++++--------- build_helpers/install_ta-lib.sh | 10 ++++++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index b44fef7a1..a452d245b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,15 +10,11 @@ services: env: global: - IMAGE_NAME=freqtradeorg/freqtrade -addons: - apt: - packages: - - libelf-dev - - libdw-dev - - binutils-dev install: -- cd build_helpers && ./install_ta-lib.sh; cd .. -- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH +- cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. +- export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH +- export TA_LIBRARY_PATH=${HOME}/dependencies/lib +- export TA_INCLUDE_PATH=${HOME}/dependencies/lib/include - pip install -r requirements-dev.txt - pip install -e . jobs: @@ -55,4 +51,4 @@ notifications: cache: pip: True directories: - - /usr/local/lib/ + - $HOME/dependencies diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index 9fe341bba..cb86e5f64 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -1,8 +1,14 @@ -if [ ! -f "/usr/local/lib/libta_lib.a" ]; then +if [ -z "$1" ]; then + INSTALL_LOC=/usr/local +else + INSTALL_LOC=${1} +fi +echo "Installing to ${INSTALL_LOC}" +if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ - && ./configure \ + && ./configure --prefix=${INSTALL_LOC}/ \ && make \ && which sudo && sudo make install || make install \ && cd .. From 9d471f3c9a2260bfac1be01e4655fdf0226fe4f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 06:27:38 +0200 Subject: [PATCH 078/269] Fix documentation for strategy-list --- docs/backtesting.md | 10 +++++++++- docs/bot-usage.md | 2 +- freqtrade/configuration/cli_options.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 179bcee15..7e9f7ff53 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -57,7 +57,15 @@ freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 freqtrade -s TestStrategy backtesting ``` -Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory +Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory. + +#### Comparing multiple Strategies + +```bash +freqtrade backtesting --strategy-list TestStrategy1 AwesomeStrategy --ticker-interval 5m +``` + +Where `TestStrategy1` and `AwesomeStrategy` refer to class names of strategies. #### Exporting trades to file diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8877ed010..0ca2f3cc5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -165,7 +165,7 @@ optional arguments: number). -l, --live Use live data. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] - Provide a commaseparated list of strategies to + Provide a space-separated list of strategies to backtest Please note that ticker-interval needs to be set either in config or via command line. When using this together with --export trades, the strategy-name diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 04fde2051..04554c386 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -130,7 +130,7 @@ AVAILABLE_CLI_OPTIONS = { ), "strategy_list": Arg( '--strategy-list', - help='Provide a comma-separated list of strategies to backtest. ' + help='Provide a space-separated list of strategies to backtest. ' 'Please note that ticker-interval needs to be set either in config ' 'or via command line. When using this together with `--export trades`, ' 'the strategy-name is injected into the filename ' From 7e91a0f4a85c0a6e66222e7a9245cf9a011eb755 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 06:45:44 +0200 Subject: [PATCH 079/269] Fail gracefully if ticker-interval is not set --- freqtrade/optimize/backtesting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 429633f31..43b44e376 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,8 +10,8 @@ from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame -from tabulate import tabulate +from freqtrade import OperationalException from freqtrade.configuration import Arguments from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider @@ -21,6 +21,7 @@ from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import IStrategy, SellType +from tabulate import tabulate logger = logging.getLogger(__name__) @@ -88,6 +89,9 @@ class Backtesting(object): Load strategy into backtesting """ self.strategy = strategy + if "ticker_interval" not in self.config: + raise OperationalException("Ticker-interval needs to be set in either configuration " + "or as cli argument `--ticker-interval 5m`") self.ticker_interval = self.config.get('ticker_interval') self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) From 81f773054de3e314908654a777c41f980eb2e8b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 06:56:08 +0200 Subject: [PATCH 080/269] Add test to verify ticker_inteval is set --- freqtrade/tests/optimize/test_backtesting.py | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 37757743e..1ae1969f4 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -9,7 +9,7 @@ import pandas as pd import pytest from arrow import Arrow -from freqtrade import DependencyException, constants +from freqtrade import DependencyException, OperationalException, constants from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi @@ -21,7 +21,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) @@ -345,6 +346,23 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert not backtesting.strategy.order_types["stoploss_on_exchange"] +def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None: + """ + Check that stoploss_on_exchange is set to False while backtesting + since backtesting assumes a perfect stoploss anyway. + """ + patch_exchange(mocker) + del default_conf['ticker_interval'] + default_conf['strategy_list'] = ['DefaultStrategy', + 'TestStrategy'] + + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + with pytest.raises(OperationalException): + Backtesting(default_conf) + log_has("Ticker-interval needs to be set in either configuration " + "or as cli argument `--ticker-interval 5m`", caplog.record_tuples) + + def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None: patch_exchange(mocker) timerange = TimeRange(None, 'line', 0, -100) From a90ced1f381391a9a7b69d2098460ebd88c27e0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 20:09:09 +0200 Subject: [PATCH 081/269] Since arguments are in milliseconds integer throughout ccxt. Explained here: https://github.com/ccxt/ccxt/issues/5636 fixes #2093 --- freqtrade/exchange/exchange.py | 3 ++- freqtrade/tests/exchange/test_exchange.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 37bbb778e..657f382d8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -725,7 +725,8 @@ class Exchange(object): return [] try: # Allow 5s offset to catch slight time offsets (discovered in #1185) - my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5) + # since needs to be int in milliseconds + my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000)) matched_trades = [trade for trade in my_trades if trade['order'] == order_id] return matched_trades diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 2f9e525dd..559a3bd50 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1391,6 +1391,13 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name): orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since) assert len(orders) == 1 assert orders[0]['price'] == 165 + assert api_mock.fetch_my_trades.call_count == 1 + # since argument should be + assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int) + assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC' + # Same test twice, hardcoded number and doing the same calculation + assert api_mock.fetch_my_trades.call_args[0][1] == 1525471195000 + assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_trades_for_order', 'fetch_my_trades', From caf4580346ef654d58421c4dd02a1eefb58f0264 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 20:23:32 +0200 Subject: [PATCH 082/269] Use UTC Timezone for test --- freqtrade/tests/exchange/test_exchange.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 559a3bd50..ebe5ad9df 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -2,7 +2,7 @@ # pragma pylint: disable=protected-access import copy import logging -from datetime import datetime +from datetime import datetime, timezone from random import randint from unittest.mock import MagicMock, Mock, PropertyMock @@ -11,8 +11,8 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade import (DependencyException, OperationalException, - TemporaryError, InvalidOrderException) +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange.exchange import API_RETRY_COUNT from freqtrade.resolvers.exchange_resolver import ExchangeResolver @@ -1361,7 +1361,7 @@ def test_name(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_trades_for_order(default_conf, mocker, exchange_name): order_id = 'ABCD-ABCD' - since = datetime(2018, 5, 5) + since = datetime(2018, 5, 5, tzinfo=timezone.utc) default_conf["dry_run"] = False mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) api_mock = MagicMock() @@ -1396,7 +1396,7 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name): assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int) assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC' # Same test twice, hardcoded number and doing the same calculation - assert api_mock.fetch_my_trades.call_args[0][1] == 1525471195000 + assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000 assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, From 8418dfbaed20a4794addff924585e1c2149cfbf3 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Tue, 6 Aug 2019 22:35:14 -0400 Subject: [PATCH 083/269] edits for jupyter notebook example --- .gitignore | 4 +- docs/data-analysis.md | 168 ++++++------------ environment.yml | 1 + user_data/notebooks/.gitkeep | 0 user_data/notebooks/analysis_example.ipynb | 187 +++++++++++++++++++++ 5 files changed, 239 insertions(+), 121 deletions(-) create mode 100644 user_data/notebooks/.gitkeep create mode 100644 user_data/notebooks/analysis_example.ipynb diff --git a/.gitignore b/.gitignore index 9ed046c40..3a9df9852 100644 --- a/.gitignore +++ b/.gitignore @@ -81,7 +81,6 @@ target/ # Jupyter Notebook .ipynb_checkpoints -*.ipynb # pyenv .python-version @@ -93,3 +92,6 @@ target/ .pytest_cache/ .mypy_cache/ + +#exceptions +!user_data/noteboks/*example.ipynb diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 2b6d6ed58..826c34747 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -1,164 +1,92 @@ # Analyzing bot data -After performing backtests, or after running the bot for some time, it will be interesting to analyze the results your bot generated. +You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation). -A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data. +## Strategy debugging example -The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results. +Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data. -## Strategy development problem analysis - -Debugging a strategy (are there no buy signals, ...) can be very time-consuming. -FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. - -It's recommended using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. - -The following is a full code-snippet, which will be explained by both comments, and step by step below. +### Import requirements and define variables used in the script ```python -# Some necessary imports +# Imports from pathlib import Path - +import os from freqtrade.data.history import load_pair_history from freqtrade.resolvers import StrategyResolver +from freqtrade.data.btanalysis import load_backtest_data +from freqtrade.data.btanalysis import load_trades_from_db + # Define some constants -ticker_interval = "5m" - +ticker_interval = "1m" # Name of the strategy class -strategyname = 'Awesomestrategy' +strategy_name = 'NewStrategy' +# Path to user data +user_data_dir = 'user_data' # Location of the strategy -strategy_location = '../xmatt/strategies' +strategy_location = os.path.join(user_data_dir, 'strategies') # Location of the data -data_location = '../freqtrade/user_data/data/binance/' +data_location = os.path.join(user_data_dir, 'data', 'binance') +# Pair to analyze # Only use one pair here -pair = "XRP_ETH" +pair = "BTC_USDT" +``` -### End constants +### Load exchange data -# Load data +```python +# Load data using values set above bt_data = load_pair_history(datadir=Path(data_location), - ticker_interval = ticker_interval, + ticker_interval=ticker_interval, pair=pair) -print(len(bt_data)) - -### Start strategy reload -# Load strategy - best done in a new cell -# Rerun each time the strategy-file is changed. -strategy = StrategyResolver({'strategy': strategyname, - 'user_data_dir': Path.cwd(), - 'strategy_path': location}).strategy - -# Run strategy (just like in backtesting) -df = strategy.analyze_ticker(bt_data, {'pair': pair}) -print(f"Generated {df['buy'].sum()} buy signals") - -# Reindex data to be "nicer" and show data -data = df.set_index('date', drop=True) -data.tail() +# Confirm success +print("Loaded " + str(len(bt_data)) + f" rows of data for {pair} from {data_location}") ``` -### Explanation +### Load and run strategy -#### Imports and constant definition +* Rerun each time the strategy file is changed +* Display the trade details. Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. -``` python -# Some necessary imports -from pathlib import Path - -from freqtrade.data.history import load_pair_history -from freqtrade.resolvers import StrategyResolver -# Define some constants -ticker_interval = "5m" - -# Name of the strategy class -strategyname = 'Awesomestrategy' -# Location of the strategy -strategy_location = 'user_data/strategies' -# Location of the data -data_location = 'user_data/data/binance' -# Only use one pair here -pair = "XRP_ETH" -``` - -This first section imports necessary modules, and defines some constants you'll probably need to adjust for your case. - -#### Load candles - -``` python -# Load data -bt_data = load_pair_history(datadir=Path(data_location), - ticker_interval = ticker_interval, - pair=pair) -print(len(bt_data)) -``` - -This second section loads the historic data and prints the amount of candles in the DataFrame. -You can also inspect this dataframe by using `bt_data.head()` or `bt_data.tail()`. - -#### Run strategy and analyze results - -Now, it's time to load and run your strategy. -For this, I recommend using a new cell in your notebook, since you'll want to repeat this until you're satisfied with your strategy. - -``` python -# Load strategy - best done in a new cell -# Needs to be ran each time the strategy-file is changed. -strategy = StrategyResolver({'strategy': strategyname, - 'user_data_dir': Path.cwd(), - 'strategy_path': location}).strategy - -# Run strategy (just like in backtesting) -df = strategy.analyze_ticker(bt_data, {'pair': pair}) -print(f"Generated {df['buy'].sum()} buy signals") - -# Reindex data to be "nicer" and show data -data = df.set_index('date', drop=True) -data.tail() -``` - -The code snippet loads and analyzes the strategy, calculates and prints the number of buy signals. - -The last 2 lines serve to analyze the dataframe in detail. -This can be important if your strategy did not generate any buy signals. -Note that using `data.head()` would also work, however this is misleading since most indicators have some "startup" time at the start of a backtested dataframe. - -There can be many things wrong, some signs to look for are: +Some possible problems: * Columns with NaN values at the end of the dataframe * Columns used in `crossed*()` functions with completely different units -## Backtesting +```python +# Load strategy using values set above +strategy = StrategyResolver({'strategy': strategy_name, + 'user_data_dir': user_data_dir, + 'strategy_path': strategy_location}).strategy -To analyze your backtest results, you can [export the trades](#exporting-trades-to-file). -You can then load the trades to perform further analysis. +# Run strategy (just like in backtesting) +df = strategy.analyze_ticker(bt_data, {'pair': pair}) -Freqtrade provides the `load_backtest_data()` helper function to easily load the backtest results, which takes the path to the the backtest-results file as parameter. +# Report results +print(f"Generated {df['buy'].sum()} buy signals") +data = df.set_index('date', drop=True) +data.tail() +``` -``` python -from freqtrade.data.btanalysis import load_backtest_data -df = load_backtest_data("user_data/backtest-result.json") +### Load backtest results into a pandas dataframe + +```python +# Load backtest results +df = load_backtest_data("user_data/backtest_data/backtest-result.json") # Show value-counts per pair df.groupby("pair")["sell_reason"].value_counts() - ``` -This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload. - -If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a Pull Request so the community can benefit from it. - -## Live data - -To analyze the trades your bot generated, you can load them to a DataFrame as follows: +### Load live trading results into a pandas dataframe ``` python -from freqtrade.data.btanalysis import load_trades_from_db - +# Fetch trades from database df = load_trades_from_db("sqlite:///tradesv3.sqlite") +# Display results df.groupby("pair")["sell_reason"].value_counts() - ``` Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. diff --git a/environment.yml b/environment.yml index 8f43b6991..cd3350fd5 100644 --- a/environment.yml +++ b/environment.yml @@ -37,6 +37,7 @@ dependencies: - coveralls - mypy # Useful for jupyter + - jupyter - ipykernel - isort - yapf diff --git a/user_data/notebooks/.gitkeep b/user_data/notebooks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb new file mode 100644 index 000000000..f9c9b27bc --- /dev/null +++ b/user_data/notebooks/analysis_example.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy debugging example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change directory\n", + "# Define all paths relative to the project root shown in the cell output\n", + "import os\n", + "try:\n", + "\tos.chdir(os.path.join(os.getcwd(), '../..'))\n", + "\tprint(os.getcwd())\n", + "except:\n", + "\tpass\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import requirements and define variables used in the script" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Imports\n", + "from pathlib import Path\n", + "import os\n", + "from freqtrade.data.history import load_pair_history\n", + "from freqtrade.resolvers import StrategyResolver\n", + "from freqtrade.data.btanalysis import load_backtest_data\n", + "from freqtrade.data.btanalysis import load_trades_from_db\n", + "\n", + "# Define some constants\n", + "ticker_interval = \"1m\"\n", + "# Name of the strategy class\n", + "strategy_name = 'NewStrategy'\n", + "# Path to user data\n", + "user_data_dir = 'user_data'\n", + "# Location of the strategy\n", + "strategy_location = os.path.join(user_data_dir, 'strategies')\n", + "# Location of the data\n", + "data_location = os.path.join(user_data_dir, 'data', 'binance')\n", + "# Pair to analyze \n", + "# Only use one pair here\n", + "pair = \"BTC_USDT\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load exchange data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load data using values set above\n", + "bt_data = load_pair_history(datadir=Path(data_location),\n", + " ticker_interval=ticker_interval,\n", + " pair=pair)\n", + "\n", + "# Confirm success\n", + "print(\"Loaded \" + str(len(bt_data)) + f\" rows of data for {pair} from {data_location}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load and run strategy \n", + "\n", + "* Rerun each time the strategy file is changed\n", + "* Display the trade details. Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n", + "\n", + "Some possible problems:\n", + "\n", + "* Columns with NaN values at the end of the dataframe\n", + "* Columns used in `crossed*()` functions with completely different units" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load strategy using values set above\n", + "strategy = StrategyResolver({'strategy': strategy_name,\n", + " 'user_data_dir': user_data_dir,\n", + " 'strategy_path': strategy_location}).strategy\n", + "\n", + "# Run strategy (just like in backtesting)\n", + "df = strategy.analyze_ticker(bt_data, {'pair': pair})\n", + "\n", + "# Report results\n", + "print(f\"Generated {df['buy'].sum()} buy signals\")\n", + "data = df.set_index('date', drop=True)\n", + "data.tail()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load backtest results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load backtest results\n", + "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", + "\n", + "# Show value-counts per pair\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load live trading results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch trades from database\n", + "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", + "\n", + "# Display results\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + ] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} From dfce20203436e01cf666b89f56a16e6e95f7e1a1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 7 Aug 2019 07:02:15 +0000 Subject: [PATCH 084/269] Update mkdocs-material from 3.1.0 to 4.4.0 --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index c4d8c2cae..ce76d52e5 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1 +1 @@ -mkdocs-material==3.1.0 \ No newline at end of file +mkdocs-material==4.4.0 \ No newline at end of file From 33bc8a24049836d9dadafec80f9212f964f84c03 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 7 Aug 2019 07:02:19 +0000 Subject: [PATCH 085/269] Update ccxt from 1.18.1008 to 1.18.1021 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index e5ef27a38..1413539c3 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.18.1008 +ccxt==1.18.1021 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.4 From 5864968ce91882fa45a311c447ac4bc4c9f446d4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 7 Aug 2019 07:02:25 +0000 Subject: [PATCH 086/269] Update plotly from 4.0.0 to 4.1.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index a6753fc3f..f10bfac3f 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.0.0 +plotly==4.1.0 From 7d02580a2bb359ea1c793c658dbbe857cf0a5a0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:03:03 +0200 Subject: [PATCH 087/269] setup.sh script shall fail if venv initialization fails --- setup.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/setup.sh b/setup.sh index fe7110ef6..908e77767 100755 --- a/setup.sh +++ b/setup.sh @@ -39,15 +39,13 @@ function updateenv() { echo "-------------------------" source .env/bin/activate echo "pip install in-progress. Please wait..." - # Install numpy first to have py_find_1st install clean - ${PYTHON} -m pip install --upgrade pip numpy - ${PYTHON} -m pip install --upgrade -r requirements.txt - + ${PYTHON} -m pip install --upgrade pip read -p "Do you want to install dependencies for dev [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then ${PYTHON} -m pip install --upgrade -r requirements-dev.txt else + ${PYTHON} -m pip install --upgrade -r requirements.txt echo "Dev dependencies ignored." fi @@ -90,7 +88,7 @@ function install_macos() { # Install bot Debian_ubuntu function install_debian() { sudo apt-get update - sudo apt-get install build-essential autoconf libtool pkg-config make wget git + sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git install_talib } @@ -129,6 +127,10 @@ function reset() { echo ${PYTHON} -m venv .env + if [ $? -ne 0 ]; then + echo "Could not create virtual environment. Leaving now" + exit 1 + fi updateenv } From cc4900f66cc8cc6298fe6b3dde250ed67e783b0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:19:16 +0200 Subject: [PATCH 088/269] Doublecheck if virtualenv IS present --- setup.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.sh b/setup.sh index 908e77767..2ca6e4460 100755 --- a/setup.sh +++ b/setup.sh @@ -37,6 +37,10 @@ function updateenv() { echo "-------------------------" echo "Updating your virtual env" echo "-------------------------" + if [ ! -f .env/bin/activate ]; then + echo "Something went wrong, no virtual environment found." + exit 1 + fi source .env/bin/activate echo "pip install in-progress. Please wait..." ${PYTHON} -m pip install --upgrade pip From 757538f1147a605f1f246821abdbfee0ac50bb9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:35:52 +0200 Subject: [PATCH 089/269] Run ldconfig to add /usr/local/lib to path --- setup.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.sh b/setup.sh index 2ca6e4460..9bdafec0d 100755 --- a/setup.sh +++ b/setup.sh @@ -72,6 +72,10 @@ function install_talib() { ./configure --prefix=/usr/local make sudo make install + if [ -x "$(command -v apt-get)" ]; then + echo "Updating library path using ldconfig" + sudo ldconfig + fi cd .. && rm -rf ./ta-lib/ cd .. } From 831e708897edf62f2950f97aa67d1ac89b2cdfdb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:44:47 +0200 Subject: [PATCH 090/269] Detect virtualenv and quit in that case --- setup.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.sh b/setup.sh index 9bdafec0d..d936e20f1 100755 --- a/setup.sh +++ b/setup.sh @@ -11,6 +11,12 @@ function check_installed_pip() { # Check which python version is installed function check_installed_python() { + if [ -n "${VIRTUAL_ENV}" ]; then + echo "Please deactivate your virtual environment before running setup.sh." + echo "You can do this by running 'deactivate'." + exit 2 + fi + which python3.7 if [ $? -eq 0 ]; then echo "using Python 3.7" From 7a47d81b7b75f42480bf8af7b4e2f440ae6fe67a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:45:58 +0200 Subject: [PATCH 091/269] Ensure git reset --hard is realy desired --- setup.sh | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/setup.sh b/setup.sh index d936e20f1..c4b6e074a 100755 --- a/setup.sh +++ b/setup.sh @@ -117,28 +117,33 @@ function reset() { echo "----------------------------" echo "Reseting branch and virtual env" echo "----------------------------" + if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ] then - if [ -d ".env" ]; then - echo "- Delete your previous virtual env" - rm -rf .env - fi - git fetch -a + read -p "Reset git branch? (This will remove all changes you made!) [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]]; then - if [ "1" == $(git branch -vv |grep -c "* develop") ] - then - echo "- Hard resetting of 'develop' branch." - git reset --hard origin/develop - elif [ "1" == $(git branch -vv |grep -c "* master") ] - then - echo "- Hard resetting of 'master' branch." - git reset --hard origin/master + git fetch -a + + if [ "1" == $(git branch -vv |grep -c "* develop") ] + then + echo "- Hard resetting of 'develop' branch." + git reset --hard origin/develop + elif [ "1" == $(git branch -vv |grep -c "* master") ] + then + echo "- Hard resetting of 'master' branch." + git reset --hard origin/master + fi fi else echo "Reset ignored because you are not on 'master' or 'develop'." fi + if [ -d ".env" ]; then + echo "- Delete your previous virtual env" + rm -rf .env + fi echo ${PYTHON} -m venv .env if [ $? -ne 0 ]; then From 9df1c23c71639bd95a171b861ac604ae58d769a4 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Wed, 7 Aug 2019 19:48:55 -0400 Subject: [PATCH 092/269] changed Path, added jupyter --- setup.py | 2 ++ user_data/notebooks/analysis_example.ipynb | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 202e3fd0d..b46101c0f 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,8 @@ setup(name='freqtrade', 'dev': all_extra, 'plot': plot, 'all': all_extra, + 'jupyter': [], + }, include_package_data=True, zip_safe=False, diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index f9c9b27bc..298269c7a 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -20,7 +20,7 @@ "\tos.chdir(os.path.join(os.getcwd(), '../..'))\n", "\tprint(os.getcwd())\n", "except:\n", - "\tpass\n" + "\tpass" ] }, { @@ -38,7 +38,6 @@ "source": [ "# Imports\n", "from pathlib import Path\n", - "import os\n", "from freqtrade.data.history import load_pair_history\n", "from freqtrade.resolvers import StrategyResolver\n", "from freqtrade.data.btanalysis import load_backtest_data\n", @@ -51,9 +50,9 @@ "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", - "strategy_location = os.path.join(user_data_dir, 'strategies')\n", + "strategy_location = Path(user_data_dir, 'strategies')\n", "# Location of the data\n", - "data_location = os.path.join(user_data_dir, 'data', 'binance')\n", + "data_location = Path(user_data_dir, 'data', 'binance')\n", "# Pair to analyze \n", "# Only use one pair here\n", "pair = \"BTC_USDT\"" From 2bc67b4a96876cefd55efc6ded6363e94968c670 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Wed, 7 Aug 2019 20:47:37 -0400 Subject: [PATCH 093/269] missed a call of os.path. removed it. --- user_data/notebooks/analysis_example.ipynb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index 298269c7a..d14575e97 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -16,8 +16,9 @@ "# Change directory\n", "# Define all paths relative to the project root shown in the cell output\n", "import os\n", + "from pathlib import Path\n", "try:\n", - "\tos.chdir(os.path.join(os.getcwd(), '../..'))\n", + "\tos.chdir(Path(os.getcwd(), '../..'))\n", "\tprint(os.getcwd())\n", "except:\n", "\tpass" From 0d4a2c6c3af13d614cca871661fd2a4daeab4f64 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 8 Aug 2019 22:45:37 +0300 Subject: [PATCH 094/269] advanced sample hyperopt added; changes to helpstrings --- freqtrade/optimize/default_hyperopt.py | 3 +- user_data/hyperopts/sample_hyperopt.py | 16 +- .../hyperopts/sample_hyperopt_advanced.py | 261 ++++++++++++++++++ 3 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 user_data/hyperopts/sample_hyperopt_advanced.py diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 1d4c40916..e05dfc95c 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -13,10 +13,9 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt class DefaultHyperOpts(IHyperOpt): """ - Default hyperopt provided by freqtrade bot. + Default hyperopt provided by the Freqtrade bot. You can override it with your own hyperopt """ - @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 9a755e52e..1a3823afa 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -14,25 +14,27 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -# This class is a sample. Feel free to customize it. class SampleHyperOpts(IHyperOpt): """ - This is a test hyperopt to inspire you. + This is a sample hyperopt to inspire you. + Feel free to customize it. More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md - You can: - - Rename the class name. + You should: + - Rename the class name to some unique name. - Add any methods you want to build your hyperopt. - 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 were moved to the parent class, you may - override them here if you need it. + 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 populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/user_data/hyperopts/sample_hyperopt_advanced.py new file mode 100644 index 000000000..00062a58d --- /dev/null +++ b/user_data/hyperopts/sample_hyperopt_advanced.py @@ -0,0 +1,261 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +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 +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +class AdvancedSampleHyperOpts(IHyperOpt): + """ + This is a sample hyperopt to inspire you. + Feel free to customize it. + + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md + + You should: + - Rename the class name to some unique name. + - Add any methods you want to build your hyperopt. + - 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. + + This sample illustrates how to override these methods. + """ + @staticmethod + def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['adx'] = ta.ADX(dataframe) + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['mfi'] = ta.MFI(dataframe) + dataframe['rsi'] = ta.RSI(dataframe) + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_upperband'] = bollinger['upper'] + dataframe['sar'] = ta.SAR(dataframe) + return dataframe + + @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 + if 'mfi-enabled' in params and params['mfi-enabled']: + conditions.append(dataframe['mfi'] < params['mfi-value']) + if 'fastd-enabled' in params and params['fastd-enabled']: + conditions.append(dataframe['fastd'] < params['fastd-value']) + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] > params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] < params['rsi-value']) + + # 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 strategy parameters + """ + return [ + 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') + ] + + @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 + """ + # print(params) + conditions = [] + # GUARDS AND TRENDS + if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: + conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: + conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + if 'sell-adx-enabled' in params and params['sell-adx-enabled']: + conditions.append(dataframe['adx'] < params['sell-adx-value']) + if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + + # 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 [ + 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') + ] + + @staticmethod + def generate_roi_table(params: Dict) -> Dict[int, float]: + """ + Generate the ROI table that will be used by Hyperopt + + This implementation generates the default legacy Freqtrade ROI tables. + + Change it if you need different number of steps in the generated + ROI tables or other structure of the ROI tables. + + Please keep it aligned with parameters in the 'roi' optimization + hyperspace defined by the roi_space method. + """ + roi_table = {} + roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] + roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] + roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] + roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 + + return roi_table + + @staticmethod + def roi_space() -> List[Dimension]: + """ + Values to search for each ROI steps + + Override it if you need some different ranges for the parameters in the + 'roi' optimization hyperspace. + + Please keep it aligned with the implementation of the + generate_roi_table method. + """ + return [ + Integer(10, 120, name='roi_t1'), + Integer(10, 60, name='roi_t2'), + Integer(10, 40, name='roi_t3'), + Real(0.01, 0.04, name='roi_p1'), + Real(0.01, 0.07, name='roi_p2'), + Real(0.01, 0.20, name='roi_p3'), + ] + + @staticmethod + def stoploss_space() -> List[Dimension]: + """ + Stoploss Value to search + + Override it if you need some different range for the parameter in the + 'stoploss' optimization hyperspace. + """ + return [ + Real(-0.5, -0.02, name='stoploss'), + ] + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators. Should be a copy of from strategy + must align to populate_indicators in this file + Only used when --spaces does not include buy + """ + dataframe.loc[ + ( + (dataframe['close'] < dataframe['bb_lowerband']) & + (dataframe['mfi'] < 16) & + (dataframe['adx'] > 25) & + (dataframe['rsi'] < 21) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators. Should be a copy of from strategy + must align to populate_indicators in this file + Only used when --spaces does not include sell + """ + dataframe.loc[ + ( + (qtpylib.crossed_above( + dataframe['macdsignal'], dataframe['macd'] + )) & + (dataframe['fastd'] > 54) + ), + 'sell'] = 1 + return dataframe From 8ad5afd3a1057ec4d118aef9ce21eb9d08851603 Mon Sep 17 00:00:00 2001 From: Cedric Schmeits Date: Thu, 8 Aug 2019 22:10:51 +0200 Subject: [PATCH 095/269] As -sharp_ratio is returned the value should be nagative. This leads in a high positive result of the loss function, as it is a minimal optimizer --- freqtrade/optimize/hyperopt_loss_sharpe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index f74b27744..5631a75de 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -39,7 +39,7 @@ class SharpeHyperOptLoss(IHyperOptLoss): sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) else: # Define high (negative) sharpe ratio to be clear that this is NOT optimal. - sharp_ratio = 20. + sharp_ratio = -20. # print(expected_yearly_return, np.std(total_profit), sharp_ratio) return -sharp_ratio From ccf3c6987435827af3649d19909fe78a0a3f4f25 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Thu, 8 Aug 2019 22:09:15 -0400 Subject: [PATCH 096/269] edits to clarify backtesting analysis --- docs/data-analysis.md | 70 +++++--- setup.py | 12 +- user_data/notebooks/analysis_example.ipynb | 195 +++++++++++++-------- 3 files changed, 176 insertions(+), 101 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 826c34747..3b07d77a6 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -2,11 +2,33 @@ You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation). +## Example snippets + +### Load backtest results into a pandas dataframe + +```python +# Load backtest results +df = load_backtest_data("user_data/backtest_data/backtest-result.json") + +# Show value-counts per pair +df.groupby("pair")["sell_reason"].value_counts() +``` + +### Load live trading results into a pandas dataframe + +``` python +# Fetch trades from database +df = load_trades_from_db("sqlite:///tradesv3.sqlite") + +# Display results +df.groupby("pair")["sell_reason"].value_counts() +``` + ## Strategy debugging example Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data. -### Import requirements and define variables used in the script +### Import requirements and define variables used in analyses ```python # Imports @@ -47,12 +69,6 @@ print("Loaded " + str(len(bt_data)) + f" rows of data for {pair} from {data_loca ### Load and run strategy * Rerun each time the strategy file is changed -* Display the trade details. Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. - -Some possible problems: - -* Columns with NaN values at the end of the dataframe -* Columns used in `crossed*()` functions with completely different units ```python # Load strategy using values set above @@ -60,33 +76,31 @@ strategy = StrategyResolver({'strategy': strategy_name, 'user_data_dir': user_data_dir, 'strategy_path': strategy_location}).strategy -# Run strategy (just like in backtesting) +# Generate buy/sell signals using strategy df = strategy.analyze_ticker(bt_data, {'pair': pair}) +``` +### Display the trade details + +* Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. + +#### Some possible problems + +* Columns with NaN values at the end of the dataframe +* Columns used in `crossed*()` functions with completely different units + +#### Comparison with full backtest + +having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting. + +Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). +The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available. + +```python # Report results print(f"Generated {df['buy'].sum()} buy signals") data = df.set_index('date', drop=True) data.tail() ``` -### Load backtest results into a pandas dataframe - -```python -# Load backtest results -df = load_backtest_data("user_data/backtest_data/backtest-result.json") - -# Show value-counts per pair -df.groupby("pair")["sell_reason"].value_counts() -``` - -### Load live trading results into a pandas dataframe - -``` python -# Fetch trades from database -df = load_trades_from_db("sqlite:///tradesv3.sqlite") - -# Display results -df.groupby("pair")["sell_reason"].value_counts() -``` - Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. diff --git a/setup.py b/setup.py index b46101c0f..6ac7a117f 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,15 @@ develop = [ 'pytest-random-order', ] -all_extra = api + plot + develop +jupyter = [ + 'jupyter', + 'nbstripout', + 'ipykernel', + 'isort', + 'yapf', + ] + +all_extra = api + plot + develop + jupyter setup(name='freqtrade', version=__version__, @@ -68,7 +76,7 @@ setup(name='freqtrade', 'dev': all_extra, 'plot': plot, 'all': all_extra, - 'jupyter': [], + 'jupyter': jupyter, }, include_package_data=True, diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index d14575e97..b3a61bc51 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -4,31 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Strategy debugging example" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Change directory\n", - "# Define all paths relative to the project root shown in the cell output\n", - "import os\n", - "from pathlib import Path\n", - "try:\n", - "\tos.chdir(Path(os.getcwd(), '../..'))\n", - "\tprint(os.getcwd())\n", - "except:\n", - "\tpass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Import requirements and define variables used in the script" + "# Analyzing bot data\n", + "\n", + "You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation)." ] }, { @@ -39,11 +17,97 @@ "source": [ "# Imports\n", "from pathlib import Path\n", + "import os\n", "from freqtrade.data.history import load_pair_history\n", "from freqtrade.resolvers import StrategyResolver\n", "from freqtrade.data.btanalysis import load_backtest_data\n", - "from freqtrade.data.btanalysis import load_trades_from_db\n", + "from freqtrade.data.btanalysis import load_trades_from_db" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change directory\n", + "# Define all paths relative to the project root shown in the cell output\n", + "try:\n", + "\tos.chdir(Path(Path.cwd(), '../..'))\n", + "\tprint(Path.cwd())\n", + "except:\n", + "\tpass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example snippets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load backtest results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load backtest results\n", + "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", "\n", + "# Show value-counts per pair\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load live trading results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch trades from database\n", + "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", + "\n", + "# Display results\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy debugging example\n", + "\n", + "Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import requirements and define variables used in analyses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "# Define some constants\n", "ticker_interval = \"1m\"\n", "# Name of the strategy class\n", @@ -51,9 +115,9 @@ "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", - "strategy_location = Path(user_data_dir, 'strategies')\n", + "strategy_location = os.path.join(user_data_dir, 'strategies')\n", "# Location of the data\n", - "data_location = Path(user_data_dir, 'data', 'binance')\n", + "data_location = os.path.join(user_data_dir, 'data', 'binance')\n", "# Pair to analyze \n", "# Only use one pair here\n", "pair = \"BTC_USDT\"" @@ -85,15 +149,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Load and run strategy \n", - "\n", - "* Rerun each time the strategy file is changed\n", - "* Display the trade details. Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n", - "\n", - "Some possible problems:\n", - "\n", - "* Columns with NaN values at the end of the dataframe\n", - "* Columns used in `crossed*()` functions with completely different units" + "### Load and run strategy\n", + "* Rerun each time the strategy file is changed" ] }, { @@ -107,53 +164,49 @@ " 'user_data_dir': user_data_dir,\n", " 'strategy_path': strategy_location}).strategy\n", "\n", - "# Run strategy (just like in backtesting)\n", - "df = strategy.analyze_ticker(bt_data, {'pair': pair})\n", + "# Generate buy/sell signals using strategy\n", + "df = strategy.analyze_ticker(bt_data, {'pair': pair})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Display the trade details\n", + "* Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n", "\n", + "#### Some possible problems\n", + "\n", + "* Columns with NaN values at the end of the dataframe\n", + "* Columns used in `crossed*()` functions with completely different units\n", + "\n", + "#### Comparison with full backtest\n", + "\n", + "having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.\n", + "\n", + "Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple \"buy\" signals for each pair in sequence (until rsi returns > 29).\n", + "The bot will only buy on the first of these signals (and also only if a trade-slot (\"max_open_trades\") is still available), or on one of the middle signals, as soon as a \"slot\" becomes available.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "# Report results\n", "print(f\"Generated {df['buy'].sum()} buy signals\")\n", "data = df.set_index('date', drop=True)\n", "data.tail()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load backtest results into a pandas dataframe" - ] - }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# Load backtest results\n", - "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", - "\n", - "# Show value-counts per pair\n", - "df.groupby(\"pair\")[\"sell_reason\"].value_counts()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load live trading results into a pandas dataframe" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Fetch trades from database\n", - "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", - "\n", - "# Display results\n", - "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + "Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data." ] } ], From 15cf5ac2d74a05cf83df56a4f9a9978e6e5f78bf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 9 Aug 2019 09:31:30 +0300 Subject: [PATCH 097/269] docs improved --- docs/hyperopt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0304c3188..6ef68d82f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -350,7 +350,7 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: ### Understand Hyperopt ROI results -If you are optimizing ROI, your result will look as follows and include a ROI table: +If you are optimizing ROI (i.e. if optimization search-space contains 'all' or 'roi'), your result will look as follows and include a ROI table: ``` Best result: @@ -396,7 +396,7 @@ Override the `generate_roi_table()` and `roi_space()` methods and implement your ### Understand Hyperopt Stoploss results -If you are optimizing stoploss values, your result will look as follows and include stoploss: +If you are optimizing stoploss values (i.e. if optimization search-space contains 'all' or 'stoploss'), your result will look as follows and include stoploss: ``` Best result: From ae39f6fba510bf1d285433e18a91b75ecd996dc2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 9 Aug 2019 14:48:57 +0300 Subject: [PATCH 098/269] use of termcolor eliminated --- freqtrade/optimize/hyperopt.py | 11 ++++++----- requirements-common.txt | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index fc0fb56b2..802c848f1 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -14,11 +14,11 @@ from pprint import pprint from typing import Any, Dict, List, Optional from colorama import init as colorama_init +from colorama import Fore, Style from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from termcolor import colored from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe @@ -163,11 +163,11 @@ class Hyperopt(Backtesting): # Colorize output if self.config.get('print_colorized', False): if results['total_profit'] > 0: - log_str = colored(log_str, attrs=['bold']) + log_str = Style.BRIGHT + log_str if results['loss'] >= MAX_LOSS: - log_str = colored(log_str, 'red') + log_str = Fore.RED + log_str elif is_best_loss: - log_str = colored(log_str, 'green') + log_str = Fore.GREEN + log_str if print_all: print(log_str) else: @@ -355,7 +355,8 @@ class Hyperopt(Backtesting): opt = self.get_optimizer(config_jobs) - colorama_init() + if self.config.get('print_colorized', False): + colorama_init(autoreset=True) try: with Parallel(n_jobs=config_jobs) as parallel: diff --git a/requirements-common.txt b/requirements-common.txt index b0c4c6729..9b62cae5c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -32,5 +32,4 @@ sdnotify==0.3.2 flask==1.1.1 # Support for colorized terminal output -termcolor==1.1.0 colorama==0.4.1 From 51d59e673b277c5bd0fa4bb343b99feea8c6453c Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 11:36:53 -0400 Subject: [PATCH 099/269] fixed another instance of Path in docs and nb --- docs/data-analysis.md | 4 ++-- user_data/notebooks/analysis_example.ipynb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 3b07d77a6..f79d1674e 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -46,9 +46,9 @@ strategy_name = 'NewStrategy' # Path to user data user_data_dir = 'user_data' # Location of the strategy -strategy_location = os.path.join(user_data_dir, 'strategies') +strategy_location = Path(user_data_dir, 'strategies') # Location of the data -data_location = os.path.join(user_data_dir, 'data', 'binance') +data_location = Path(user_data_dir, 'data', 'binance') # Pair to analyze # Only use one pair here pair = "BTC_USDT" diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index b3a61bc51..b8bcdd7bf 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -115,9 +115,9 @@ "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", - "strategy_location = os.path.join(user_data_dir, 'strategies')\n", + "strategy_location = Path(user_data_dir, 'strategies')\n", "# Location of the data\n", - "data_location = os.path.join(user_data_dir, 'data', 'binance')\n", + "data_location = Path(user_data_dir, 'data', 'binance')\n", "# Pair to analyze \n", "# Only use one pair here\n", "pair = \"BTC_USDT\"" From 247d7475e17cff5f18e3dc8e31667cbeac44e3a9 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 11:41:05 -0400 Subject: [PATCH 100/269] fixes to example notebook. --- user_data/notebooks/analysis_example.ipynb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index b8bcdd7bf..a92855add 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -33,10 +33,10 @@ "# Change directory\n", "# Define all paths relative to the project root shown in the cell output\n", "try:\n", - "\tos.chdir(Path(Path.cwd(), '../..'))\n", - "\tprint(Path.cwd())\n", + " os.chdir(Path(Path.cwd(), '../..'))\n", + " print(Path.cwd())\n", "except:\n", - "\tpass" + " pass" ] }, { @@ -109,7 +109,7 @@ "outputs": [], "source": [ "# Define some constants\n", - "ticker_interval = \"1m\"\n", + "ticker_interval = \"5m\"\n", "# Name of the strategy class\n", "strategy_name = 'NewStrategy'\n", "# Path to user data\n", @@ -201,10 +201,8 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ "Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data." ] From 3cc772c8e9eead8a11cc7208a84bdaca6f580240 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 11:53:29 -0400 Subject: [PATCH 101/269] added reminders --- docs/data-analysis.md | 2 ++ user_data/notebooks/analysis_example.ipynb | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index f79d1674e..2e2ebc3c6 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -2,6 +2,8 @@ You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation). +*Pro tip - Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* + ## Example snippets ### Load backtest results into a pandas dataframe diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index a92855add..30e9e1a97 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -6,7 +6,12 @@ "source": [ "# Analyzing bot data\n", "\n", - "You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation)." + "You can analyze the results of backtests and trading history easily using Jupyter notebooks. \n", + "**Copy this file so your changes don't get clobbered with the next freqtrade update!** \n", + "For usage instructions, see [jupyter.org](https://jupyter.org/documentation). \n", + "*Pro tip - Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*\n", + "\n", + "\n" ] }, { From dd35ba5e812589ec1da130681df2fb38ba87cd26 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 17:06:19 -0400 Subject: [PATCH 102/269] added imports to doc code blocks. --- docs/data-analysis.md | 9 +++++---- user_data/notebooks/analysis_example.ipynb | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 2e2ebc3c6..5db9e6c3b 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -9,6 +9,7 @@ You can analyze the results of backtests and trading history easily using Jupyte ### Load backtest results into a pandas dataframe ```python +from freqtrade.data.btanalysis import load_backtest_data # Load backtest results df = load_backtest_data("user_data/backtest_data/backtest-result.json") @@ -19,6 +20,8 @@ df.groupby("pair")["sell_reason"].value_counts() ### Load live trading results into a pandas dataframe ``` python +from freqtrade.data.btanalysis import load_trades_from_db + # Fetch trades from database df = load_trades_from_db("sqlite:///tradesv3.sqlite") @@ -38,13 +41,11 @@ from pathlib import Path import os from freqtrade.data.history import load_pair_history from freqtrade.resolvers import StrategyResolver -from freqtrade.data.btanalysis import load_backtest_data -from freqtrade.data.btanalysis import load_trades_from_db # Define some constants -ticker_interval = "1m" +ticker_interval = "5m" # Name of the strategy class -strategy_name = 'NewStrategy' +strategy_name = 'AwesomeStrategy' # Path to user data user_data_dir = 'user_data' # Location of the strategy diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index 30e9e1a97..f5e2c12d7 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -116,7 +116,7 @@ "# Define some constants\n", "ticker_interval = \"5m\"\n", "# Name of the strategy class\n", - "strategy_name = 'NewStrategy'\n", + "strategy_name = 'AwesomeStrategy'\n", "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", From 8eb39178ea2668af09a272128e3823d30910d059 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 17:24:17 -0400 Subject: [PATCH 103/269] code block instructions. removed extra packages --- docs/data-analysis.md | 5 ++++- setup.py | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 5db9e6c3b..66bb50694 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -42,6 +42,9 @@ import os from freqtrade.data.history import load_pair_history from freqtrade.resolvers import StrategyResolver +# You can override strategy settings as demonstrated below. +# Customize these according to your needs. + # Define some constants ticker_interval = "5m" # Name of the strategy class @@ -66,7 +69,7 @@ bt_data = load_pair_history(datadir=Path(data_location), pair=pair) # Confirm success -print("Loaded " + str(len(bt_data)) + f" rows of data for {pair} from {data_location}") +print(f"Loaded {len(bt_data)} rows of data for {pair} from {data_location}") ``` ### Load and run strategy diff --git a/setup.py b/setup.py index 6ac7a117f..41e1b8f45 100644 --- a/setup.py +++ b/setup.py @@ -29,8 +29,6 @@ jupyter = [ 'jupyter', 'nbstripout', 'ipykernel', - 'isort', - 'yapf', ] all_extra = api + plot + develop + jupyter From 50c9679e23d9d2fe219d7cc09e5256ffebc66b4b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 14:24:14 +0300 Subject: [PATCH 104/269] move load_config_file() to separate module --- freqtrade/configuration/configuration.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 17ad37d6a..e564c79ce 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -1,9 +1,7 @@ """ This module contains the configuration class """ -import json import logging -import sys import warnings from argparse import Namespace from typing import Any, Callable, Dict, Optional @@ -12,6 +10,7 @@ from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.create_datadir import create_datadir from freqtrade.configuration.json_schema import validate_config_schema +from freqtrade.configuration.load_config import load_config_file from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode @@ -52,24 +51,7 @@ class Configuration(object): logger.info('Using config: %s ...', path) # Merge config options, overwriting old values - config = deep_merge_dicts(self._load_config_file(path), config) - - return config - - def _load_config_file(self, path: str) -> Dict[str, Any]: - """ - Loads a config file from the given path - :param path: path as str - :return: configuration as dictionary - """ - try: - # Read config from stdin if requested in the options - with open(path) if path != '-' else sys.stdin as file: - config = json.load(file) - except FileNotFoundError: - raise OperationalException( - f'Config file "{path}" not found!' - ' Please create a config file or check whether it exists.') + config = deep_merge_dicts(load_config_file(path), config) return config From ad6a249832e2c0429fb534fa648607a705ee0f7d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 15:14:37 +0300 Subject: [PATCH 105/269] download_backtest_data.py adjusted --- scripts/download_backtest_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index ed96cec71..580592294 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -12,6 +12,7 @@ from freqtrade.configuration import Arguments, TimeRange from freqtrade.configuration import Configuration from freqtrade.configuration.arguments import ARGS_DOWNLOADER from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.load_config import load_config_file from freqtrade.data.history import download_pair_history from freqtrade.exchange import Exchange from freqtrade.misc import deep_merge_dicts @@ -42,7 +43,7 @@ if args.config: for path in args.config: logger.info(f"Using config: {path}...") # Merge config options, overwriting old values - config = deep_merge_dicts(configuration._load_config_file(path), config) + config = deep_merge_dicts(load_config_file(path), config) config['stake_currency'] = '' # Ensure we do not use Exchange credentials From 28d8fc871a14418a44521f2635809ad66dbbff3f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 15:15:09 +0300 Subject: [PATCH 106/269] tests adjusted --- freqtrade/configuration/load_config.py | 30 ++++++++++++++++++++++++++ freqtrade/tests/conftest.py | 2 +- freqtrade/tests/test_configuration.py | 14 ++++++------ 3 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 freqtrade/configuration/load_config.py diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py new file mode 100644 index 000000000..25504144f --- /dev/null +++ b/freqtrade/configuration/load_config.py @@ -0,0 +1,30 @@ +""" +This module contain functions to load the configuration file +""" +import json +import logging +import sys +from typing import Any, Dict + +from freqtrade import OperationalException + + +logger = logging.getLogger(__name__) + + +def load_config_file(path: str) -> Dict[str, Any]: + """ + Loads a config file from the given path + :param path: path as str + :return: configuration as dictionary + """ + try: + # Read config from stdin if requested in the options + with open(path) if path != '-' else sys.stdin as file: + config = json.load(file) + except FileNotFoundError: + raise OperationalException( + f'Config file "{path}" not found!' + ' Please create a config file or check whether it exists.') + + return config diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 71ed23901..88257bcae 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -45,7 +45,7 @@ def get_args(args): def patched_configuration_load_config_file(mocker, config) -> None: mocker.patch( - 'freqtrade.configuration.configuration.Configuration._load_config_file', + 'freqtrade.configuration.load_config.load_config_file', lambda *args, **kwargs: config ) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 1e76297a6..d4c8704be 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -15,6 +15,7 @@ from freqtrade.configuration import Arguments, Configuration from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.create_datadir import create_datadir from freqtrade.configuration.json_schema import validate_config_schema +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 from freqtrade.state import RunMode @@ -26,8 +27,7 @@ from freqtrade.tests.conftest import (log_has, log_has_re, def all_conf(): config_file = Path(__file__).parents[2] / "config_full.json.example" print(config_file) - configuration = Configuration(Namespace()) - conf = configuration._load_config_file(str(config_file)) + conf = load_config_file(str(config_file)) return conf @@ -53,12 +53,11 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None: - file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( + file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open( read_data=json.dumps(default_conf) )) - configuration = Configuration(Namespace()) - validated_conf = configuration._load_config_file('somefile') + validated_conf = load_config_file('somefile') assert file_mock.call_count == 1 assert validated_conf.items() >= default_conf.items() @@ -114,7 +113,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: configsmock = MagicMock(side_effect=config_files) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._load_config_file', + 'freqtrade.configuration.load_config.load_config_file', configsmock ) @@ -154,10 +153,9 @@ def test_load_config_file_exception(mocker) -> None: 'freqtrade.configuration.configuration.open', MagicMock(side_effect=FileNotFoundError('File not found')) ) - configuration = Configuration(Namespace()) with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'): - configuration._load_config_file('somefile') + load_config_file('somefile') def test_load_config(default_conf, mocker) -> None: From ab092fc77f291e1b5de9356729bf692803d59ca3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Aug 2019 15:45:41 +0200 Subject: [PATCH 107/269] Reinstate comment on backesting data --- docs/data-analysis.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 66bb50694..ecd94445b 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -17,6 +17,8 @@ df = load_backtest_data("user_data/backtest_data/backtest-result.json") df.groupby("pair")["sell_reason"].value_counts() ``` +This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload. + ### Load live trading results into a pandas dataframe ``` python From 48d83768787a7222b30b04b3d40289df143f24ab Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 18:47:58 +0300 Subject: [PATCH 108/269] tests fixed --- freqtrade/tests/conftest.py | 2 +- freqtrade/tests/test_configuration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 88257bcae..4b9bf6cd8 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -45,7 +45,7 @@ def get_args(args): def patched_configuration_load_config_file(mocker, config) -> None: mocker.patch( - 'freqtrade.configuration.load_config.load_config_file', + 'freqtrade.configuration.configuration.load_config_file', lambda *args, **kwargs: config ) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index d4c8704be..e325a0de2 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -113,7 +113,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: configsmock = MagicMock(side_effect=config_files) mocker.patch( - 'freqtrade.configuration.load_config.load_config_file', + 'freqtrade.configuration.configuration.load_config_file', configsmock ) From c4cbe79b48795fd7f045f330e76f17d37ab18cd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Aug 2019 19:55:33 +0200 Subject: [PATCH 109/269] Adjust documentation --- docs/bot-usage.md | 4 ++-- docs/data-analysis.md | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 0ca2f3cc5..4e3f55d91 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -56,8 +56,8 @@ freqtrade -c path/far/far/away/config.json The bot allows you to use multiple configuration files by specifying multiple `-c/--config` configuration options in the command line. Configuration parameters -defined in the last configuration file override parameters with the same name -defined in the previous configuration file specified in the command line. +defined in latter configuration files override parameters with the same name +defined in the earlier configuration files specified in the command line. For example, you can make a separate configuration file with your key and secrete for the Exchange you use for trading, specify default configuration file with diff --git a/docs/data-analysis.md b/docs/data-analysis.md index ecd94445b..c89353cc8 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -31,6 +31,16 @@ df = load_trades_from_db("sqlite:///tradesv3.sqlite") df.groupby("pair")["sell_reason"].value_counts() ``` +### Load multiple configuration files + +This option can be usefull to inspect the results of passing in multiple configs in case of problems + +``` python +from freqtrade.configuration import Configuration +config = Configuration.from_files(["config1.json", "config2.json"]) +print(config) +``` + ## Strategy debugging example Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data. From afba31c3f996c5885fcddf9d572e3a645f3aa026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Aug 2019 19:57:49 +0200 Subject: [PATCH 110/269] change method from _load_config_Files to from_files() --- freqtrade/configuration/configuration.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index e564c79ce..85a156fab 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -4,7 +4,7 @@ This module contains the configuration class import logging import warnings from argparse import Namespace -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Dict, Optional, List from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange @@ -39,16 +39,22 @@ class Configuration(object): return self.config - def _load_config_files(self) -> Dict[str, Any]: + @staticmethod + def from_files(files: List[str]) -> Dict[str, Any]: """ - Iterate through the config files passed in the args, - loading all of them and merging their contents. + Iterate through the config files passed in, loading all of them + and merging their contents. + Files are loaded in sequence, parameters in later configuration files + override the same parameter from an earlier file (last definition wins). + :param files: List of file paths + :return: configuration dictionary """ + # Keep this method as staticmethod, so it can be used from interactive environments config: Dict[str, Any] = {} # We expect here a list of config filenames - for path in self.args.config: - logger.info('Using config: %s ...', path) + for path in files: + logger.info(f'Using config: {path} ...') # Merge config options, overwriting old values config = deep_merge_dicts(load_config_file(path), config) @@ -69,7 +75,7 @@ class Configuration(object): :return: Configuration dictionary """ # Load all configs - config: Dict[str, Any] = self._load_config_files() + config: Dict[str, Any] = Configuration.from_files(self.args.config) # Make resulting config more canonical self._normalize_config(config) From eb328037b74847c5e911886249191be1a4c30d8b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Aug 2019 19:58:04 +0200 Subject: [PATCH 111/269] combine normalize method and config validation to in_files --- freqtrade/configuration/configuration.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 85a156fab..237346e37 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -4,7 +4,7 @@ This module contains the configuration class import logging import warnings from argparse import Namespace -from typing import Any, Callable, Dict, Optional, List +from typing import Any, Callable, Dict, List, Optional from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange @@ -59,16 +59,16 @@ class Configuration(object): # Merge config options, overwriting old values config = deep_merge_dicts(load_config_file(path), config) - return config - - def _normalize_config(self, config: Dict[str, Any]) -> None: - """ - Make config more canonical -- i.e. for example add missing parts that we expect - to be normally in it... - """ + # Normalize config if 'internals' not in config: config['internals'] = {} + # validate configuration before returning + logger.info('Validating configuration ...') + validate_config_schema(config) + + return config + def load_config(self) -> Dict[str, Any]: """ Extract information for sys.argv and load the bot configuration @@ -77,12 +77,6 @@ class Configuration(object): # Load all configs config: Dict[str, Any] = Configuration.from_files(self.args.config) - # Make resulting config more canonical - self._normalize_config(config) - - logger.info('Validating configuration ...') - validate_config_schema(config) - self._validate_config_consistency(config) self._process_common_options(config) From 6d89da45b0fe2c4db46ebf3e229ebc8b056527f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Aug 2019 20:02:11 +0200 Subject: [PATCH 112/269] Add test for from_config --- freqtrade/tests/test_configuration.py | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index e325a0de2..667bd042e 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -133,6 +133,35 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: assert log_has('Validating configuration ...', caplog.record_tuples) +def test_from_config(default_conf, mocker, caplog) -> None: + conf1 = deepcopy(default_conf) + conf2 = deepcopy(default_conf) + del conf1['exchange']['key'] + del conf1['exchange']['secret'] + del conf2['exchange']['name'] + conf2['exchange']['pair_whitelist'] += ['NANO/BTC'] + conf2['fiat_display_currency'] = "EUR" + config_files = [conf1, conf2] + + configsmock = MagicMock(side_effect=config_files) + mocker.patch( + 'freqtrade.configuration.configuration.load_config_file', + configsmock + ) + + validated_conf = Configuration.from_files(['test_conf.json', 'test2_conf.json']) + + exchange_conf = default_conf['exchange'] + assert validated_conf['exchange']['name'] == exchange_conf['name'] + assert validated_conf['exchange']['key'] == exchange_conf['key'] + assert validated_conf['exchange']['secret'] == exchange_conf['secret'] + assert validated_conf['exchange']['pair_whitelist'] != conf1['exchange']['pair_whitelist'] + assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist'] + assert validated_conf['fiat_display_currency'] == "EUR" + assert 'internals' in validated_conf + assert log_has('Validating configuration ...', caplog.record_tuples) + + def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = -1 patched_configuration_load_config_file(mocker, default_conf) From 5209ce5bfac246a3ecd44d9976e49ddbb68282e5 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 16:30:14 +0300 Subject: [PATCH 113/269] tests: don't mask numpy errors as warnings in tests --- freqtrade/tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 4b9bf6cd8..8c995b7c0 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -10,6 +10,7 @@ from unittest.mock import MagicMock, PropertyMock import arrow import pytest +import numpy as np from telegram import Chat, Message, Update from freqtrade import constants, persistence @@ -25,6 +26,10 @@ from freqtrade.worker import Worker logging.getLogger('').setLevel(logging.INFO) +# Do not mask numpy errors as warnings that no one read, raise the exсeption +np.seterr(all='raise') + + def log_has(line, logs): # caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar') # and we want to match line against foobar in the tuple From 1a85e3b4cd8ec9412a6d24454b14c489189676ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 13:46:32 +0200 Subject: [PATCH 114/269] Fix numpy warning --- freqtrade/rpc/rpc.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f77e0eddb..7b811cadc 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -10,7 +10,7 @@ from typing import Dict, Any, List, Optional import arrow import sqlalchemy as sql -from numpy import mean, nan_to_num, NAN +from numpy import mean, NAN from pandas import DataFrame from freqtrade import TemporaryError, DependencyException @@ -195,9 +195,9 @@ class RPC(object): trades = Trade.query.order_by(Trade.id).all() profit_all_coin = [] - profit_all_percent = [] + profit_all_perc = [] profit_closed_coin = [] - profit_closed_percent = [] + profit_closed_perc = [] durations = [] for trade in trades: @@ -211,7 +211,7 @@ class RPC(object): if not trade.is_open: profit_percent = trade.calc_profit_percent() profit_closed_coin.append(trade.calc_profit()) - profit_closed_percent.append(profit_percent) + profit_closed_perc.append(profit_percent) else: # Get current rate try: @@ -223,7 +223,7 @@ class RPC(object): profit_all_coin.append( trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)) ) - profit_all_percent.append(profit_percent) + profit_all_perc.append(profit_percent) best_pair = Trade.session.query( Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum') @@ -238,7 +238,8 @@ class RPC(object): # Prepare data to display profit_closed_coin_sum = round(sum(profit_closed_coin), 8) - profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2) + profit_closed_percent = (round(mean(profit_closed_perc) * 100, 2) if profit_closed_perc + else 0.0) profit_closed_fiat = self._fiat_converter.convert_amount( profit_closed_coin_sum, stake_currency, @@ -246,7 +247,7 @@ class RPC(object): ) if self._fiat_converter else 0 profit_all_coin_sum = round(sum(profit_all_coin), 8) - profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2) + profit_all_percent = round(mean(profit_all_perc) * 100, 2) if profit_all_perc else 0.0 profit_all_fiat = self._fiat_converter.convert_amount( profit_all_coin_sum, stake_currency, From 176beefa88e27ccda79bada82a0143ef7e39c3de Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 14:14:51 +0200 Subject: [PATCH 115/269] Disable stoploss on exchange for dry-runs --- freqtrade/resolvers/strategy_resolver.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index aa73327ff..4c521e2f4 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -81,7 +81,7 @@ class StrategyResolver(IResolver): key=lambda t: t[0])) self.strategy.stoploss = float(self.strategy.stoploss) - self._strategy_sanity_validations() + self._strategy_sanity_validations(config) def _override_attribute_helper(self, config, attribute: str, default): """ @@ -102,7 +102,7 @@ class StrategyResolver(IResolver): setattr(self.strategy, attribute, default) config[attribute] = default - def _strategy_sanity_validations(self): + def _strategy_sanity_validations(self, config): if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES): raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " f"Order-types mapping is incomplete.") @@ -111,6 +111,12 @@ class StrategyResolver(IResolver): raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " f"Order-time-in-force mapping is incomplete.") + # Stoploss on exchange does not make sense, therefore we need to disable that. + if config.get('dry_run'): + logger.info("Disabling stoploss_on_exchange during dry-run.") + self.strategy.order_types['stoploss_on_exchange'] = False + config['order_types']['stoploss_on_exchange'] = False + def _load_strategy( self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: """ From e02e64fc0756d719a65ac914cc859f9410fd8e48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 14:15:04 +0200 Subject: [PATCH 116/269] Add test to make sure dry-run disables stoploss on exchange --- freqtrade/tests/strategy/test_strategy.py | 39 +++++++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index df8c0f126..aa386fa6c 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -15,7 +15,7 @@ from freqtrade.resolvers import StrategyResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import IStrategy -from freqtrade.tests.conftest import log_has_re +from freqtrade.tests.conftest import log_has_re, log_has def test_import_strategy(caplog): @@ -257,12 +257,9 @@ def test_strategy_override_order_types(caplog): for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: assert resolver.strategy.order_types[method] == order_types[method] - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'order_types' with value in config file:" - " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," - " 'stoploss_on_exchange': True}." - ) in caplog.record_tuples + assert log_has("Override strategy 'order_types' with value in config file:" + " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," + " 'stoploss_on_exchange': True}.", caplog.record_tuples) config = { 'strategy': 'DefaultStrategy', @@ -275,6 +272,34 @@ def test_strategy_override_order_types(caplog): StrategyResolver(config) +def test_strategy_override_order_types_dryrun(caplog): + caplog.set_level(logging.INFO) + + order_types = { + 'buy': 'market', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': True, + } + + config = { + 'strategy': 'DefaultStrategy', + 'order_types': order_types, + 'dry_run': True, + } + resolver = StrategyResolver(config) + + assert resolver.strategy.order_types + for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: + assert resolver.strategy.order_types[method] == order_types[method] + + assert log_has("Disabling stoploss_on_exchange during dry-run.", caplog.record_tuples) + + assert log_has("Override strategy 'order_types' with value in config file:" + " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," + " 'stoploss_on_exchange': False}.", caplog.record_tuples) + + def test_strategy_override_order_tif(caplog): caplog.set_level(logging.INFO) From 4b4fcc703471890c3bb584f9a47b04402750c9f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 19:43:57 +0200 Subject: [PATCH 117/269] Change stoploss_on_exchange in freqtradebot --- freqtrade/freqtradebot.py | 8 ++++++- freqtrade/resolvers/strategy_resolver.py | 10 ++------ freqtrade/tests/strategy/test_strategy.py | 28 ----------------------- 3 files changed, 9 insertions(+), 37 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d52165e0a..603b0631f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -20,7 +20,7 @@ from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver -from freqtrade.state import State +from freqtrade.state import State, RunMode from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.wallets import Wallets @@ -75,6 +75,12 @@ class FreqtradeBot(object): persistence.init(self.config.get('db_url', None), clean_open_orders=self.config.get('dry_run', False)) + # Stoploss on exchange does not make sense, therefore we need to disable that. + if (self.dataprovider.runmode == RunMode.DRY_RUN and + self.strategy.order_types.get('stoploss_on_exchange', False)): + logger.info("Disabling stoploss_on_exchange during dry-run.") + self.strategy.order_types['stoploss_on_exchange'] = False + config['order_types']['stoploss_on_exchange'] = False # Set initial bot state from config initial_state = self.config.get('initial_state') self.state = State[initial_state.upper()] if initial_state else State.STOPPED diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 4c521e2f4..aa73327ff 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -81,7 +81,7 @@ class StrategyResolver(IResolver): key=lambda t: t[0])) self.strategy.stoploss = float(self.strategy.stoploss) - self._strategy_sanity_validations(config) + self._strategy_sanity_validations() def _override_attribute_helper(self, config, attribute: str, default): """ @@ -102,7 +102,7 @@ class StrategyResolver(IResolver): setattr(self.strategy, attribute, default) config[attribute] = default - def _strategy_sanity_validations(self, config): + def _strategy_sanity_validations(self): if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES): raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " f"Order-types mapping is incomplete.") @@ -111,12 +111,6 @@ class StrategyResolver(IResolver): raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " f"Order-time-in-force mapping is incomplete.") - # Stoploss on exchange does not make sense, therefore we need to disable that. - if config.get('dry_run'): - logger.info("Disabling stoploss_on_exchange during dry-run.") - self.strategy.order_types['stoploss_on_exchange'] = False - config['order_types']['stoploss_on_exchange'] = False - def _load_strategy( self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: """ diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index aa386fa6c..93c0a9e8a 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -272,34 +272,6 @@ def test_strategy_override_order_types(caplog): StrategyResolver(config) -def test_strategy_override_order_types_dryrun(caplog): - caplog.set_level(logging.INFO) - - order_types = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': True, - } - - config = { - 'strategy': 'DefaultStrategy', - 'order_types': order_types, - 'dry_run': True, - } - resolver = StrategyResolver(config) - - assert resolver.strategy.order_types - for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: - assert resolver.strategy.order_types[method] == order_types[method] - - assert log_has("Disabling stoploss_on_exchange during dry-run.", caplog.record_tuples) - - assert log_has("Override strategy 'order_types' with value in config file:" - " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," - " 'stoploss_on_exchange': False}.", caplog.record_tuples) - - def test_strategy_override_order_tif(caplog): caplog.set_level(logging.INFO) From a225672c8776f848597546fe945bb124f3fc4782 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 19:44:15 +0200 Subject: [PATCH 118/269] Add tests for dry-run stoposs_on_exchange --- freqtrade/tests/test_freqtradebot.py | 74 +++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 1a4c5159c..7ed918318 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType -from freqtrade.state import State +from freqtrade.state import State, RunMode from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.tests.conftest import (get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, @@ -130,7 +130,77 @@ def test_throttle_with_assets(mocker, default_conf) -> None: assert result == -1 -def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) + ) + conf = default_conf.copy() + conf['runmode'] = RunMode.DRY_RUN + conf['order_types'] = { + 'buy': 'market', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': True, + } + + freqtrade = FreqtradeBot(conf) + assert log_has("Disabling stoploss_on_exchange during dry-run.", caplog.record_tuples) + assert not freqtrade.strategy.order_types['stoploss_on_exchange'] + + caplog.clear() + # is left untouched + conf = default_conf.copy() + conf['runmode'] = RunMode.DRY_RUN + conf['order_types'] = { + 'buy': 'market', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False, + } + freqtrade = FreqtradeBot(conf) + assert not freqtrade.strategy.order_types['stoploss_on_exchange'] + assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog.record_tuples) + + +def test_order_dict_live(default_conf, mocker, caplog) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) + ) + conf = default_conf.copy() + conf['runmode'] = RunMode.LIVE + conf['order_types'] = { + 'buy': 'market', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': True, + } + + freqtrade = FreqtradeBot(conf) + assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog.record_tuples) + assert freqtrade.strategy.order_types['stoploss_on_exchange'] + + caplog.clear() + # is left untouched + conf = default_conf.copy() + conf['runmode'] = RunMode.LIVE + conf['order_types'] = { + 'buy': 'market', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False, + } + freqtrade = FreqtradeBot(conf) + assert not freqtrade.strategy.order_types['stoploss_on_exchange'] + assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog.record_tuples) + + +def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( From a1b5c7242e05aa702fc23900ae5401acceb858f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 20:14:58 +0200 Subject: [PATCH 119/269] Change log-has to use record_tuples itself --- freqtrade/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 4b9bf6cd8..fdccad02f 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -29,13 +29,13 @@ def log_has(line, logs): # caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar') # and we want to match line against foobar in the tuple return reduce(lambda a, b: a or b, - filter(lambda x: x[2] == line, logs), + filter(lambda x: x[2] == line, logs.record_tuples), False) def log_has_re(line, logs): return reduce(lambda a, b: a or b, - filter(lambda x: re.match(line, x[2]), logs), + filter(lambda x: re.match(line, x[2]), logs.record_tuples), False) From 02216073186caaa10561dc258c3cacf2685faa35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 20:16:34 +0200 Subject: [PATCH 120/269] Change log_has for some tests --- freqtrade/tests/data/test_converter.py | 12 +++++------- freqtrade/tests/data/test_history.py | 26 ++++++++++---------------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index f68224e0e..39462bdd8 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -18,7 +18,7 @@ def test_parse_ticker_dataframe(ticker_history_list, caplog): dataframe = parse_ticker_dataframe(ticker_history_list, '5m', pair="UNITTEST/BTC", fill_missing=True) assert dataframe.columns.tolist() == columns - assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples) + assert log_has('Parsing tickerlist to dataframe', caplog) def test_ohlcv_fill_up_missing_data(caplog): @@ -34,8 +34,7 @@ def test_ohlcv_fill_up_missing_data(caplog): assert (data.columns == data2.columns).all() assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " - f"{len(data)} - after: {len(data2)}", - caplog.record_tuples) + f"{len(data)} - after: {len(data2)}", caplog) # Test fillup actually fixes invalid backtest data min_date, max_date = get_timeframe({'UNITTEST/BTC': data}) @@ -97,8 +96,7 @@ def test_ohlcv_fill_up_missing_data2(caplog): assert (data.columns == data2.columns).all() assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " - f"{len(data)} - after: {len(data2)}", - caplog.record_tuples) + f"{len(data)} - after: {len(data2)}", caplog) def test_ohlcv_drop_incomplete(caplog): @@ -140,11 +138,11 @@ def test_ohlcv_drop_incomplete(caplog): data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", fill_missing=False, drop_incomplete=False) assert len(data) == 4 - assert not log_has("Dropping last candle", caplog.record_tuples) + assert not log_has("Dropping last candle", caplog) # Drop last candle data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", fill_missing=False, drop_incomplete=True) assert len(data) == 3 - assert log_has("Dropping last candle", caplog.record_tuples) + assert log_has("Dropping last candle", caplog) diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 424333e99..00f4738f7 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -64,8 +64,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf) -> None: assert isinstance(ld, DataFrame) assert not log_has( 'Download history data for pair: "UNITTEST/BTC", interval: 30m ' - 'and store in None.', - caplog.record_tuples + 'and store in None.', caplog ) @@ -76,8 +75,7 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: assert log_has( 'No history data for pair: "UNITTEST/BTC", interval: 7m. ' 'Use --refresh-pairs-cached option or download_backtest_data.py ' - 'script to download the data', - caplog.record_tuples + 'script to download the data', caplog ) @@ -89,8 +87,7 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: assert os.path.isfile(file) is True assert not log_has( 'Download history data for pair: "UNITTEST/BTC", interval: 1m ' - 'and store in None.', - caplog.record_tuples + 'and store in None.', caplog ) _clean_test_file(file) @@ -113,8 +110,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau assert log_has( 'No history data for pair: "MEME/BTC", interval: 1m. ' 'Use --refresh-pairs-cached option or download_backtest_data.py ' - 'script to download the data', - caplog.record_tuples + 'script to download the data', caplog ) # download a new pair if refresh_pairs is set @@ -126,8 +122,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau assert os.path.isfile(file) is True assert log_has( 'Download history data for pair: "MEME/BTC", interval: 1m ' - 'and store in None.', - caplog.record_tuples + 'and store in None.', caplog ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): history.load_pair_history(datadir=None, @@ -149,7 +144,7 @@ def test_load_data_live(default_conf, mocker, caplog) -> None: exchange=exchange) assert refresh_mock.call_count == 1 assert len(refresh_mock.call_args_list[0][0][0]) == 2 - assert log_has('Live: Downloading data for all defined pairs ...', caplog.record_tuples) + assert log_has('Live: Downloading data for all defined pairs ...', caplog) def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None: @@ -350,8 +345,7 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def _clean_test_file(file1_5) assert log_has( 'Failed to download history data for pair: "MEME/BTC", interval: 1m. ' - 'Error: File Error', - caplog.record_tuples + 'Error: File Error', caplog ) @@ -380,7 +374,7 @@ def test_load_partial_missing(caplog) -> None: start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0] assert log_has(f'Missing data at start for pair ' f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}', - caplog.record_tuples) + caplog) # Make sure we start fresh - test missing data at end caplog.clear() start = arrow.get('2018-01-10T00:00:00') @@ -396,7 +390,7 @@ def test_load_partial_missing(caplog) -> None: end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) assert log_has(f'Missing data at end for pair ' f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}', - caplog.record_tuples) + caplog) def test_init(default_conf, mocker) -> None: @@ -560,7 +554,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", - caplog.record_tuples) + caplog) def test_validate_backtest_data(default_conf, mocker, caplog) -> None: From d53f63023ab23536438db8f0dd4d7df46473cf6d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 20:16:52 +0200 Subject: [PATCH 121/269] Change log_has to get caplog instead of caplog.record_tuples in more tests --- freqtrade/tests/edge/test_edge.py | 4 +- freqtrade/tests/optimize/test_backtesting.py | 60 ++++++--------- freqtrade/tests/optimize/test_edge_cli.py | 28 ++----- freqtrade/tests/optimize/test_hyperopt.py | 77 +++++++------------- freqtrade/tests/strategy/test_interface.py | 33 ++++----- freqtrade/tests/strategy/test_strategy.py | 7 +- 6 files changed, 72 insertions(+), 137 deletions(-) diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index 45b8e609e..09fa1d93e 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -311,7 +311,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): assert not edge.calculate() assert len(edge._cached_pairs) == 0 - assert log_has("No data found. Edge is stopped ...", caplog.record_tuples) + assert log_has("No data found. Edge is stopped ...", caplog) assert edge._last_updated == 0 @@ -326,7 +326,7 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): assert not edge.calculate() assert len(edge._cached_pairs) == 0 - assert log_has("No trades found.", caplog.record_tuples) + assert log_has("No trades found.", caplog) def test_edge_init_error(mocker, edge_conf,): diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 71d460621..9ed7e7296 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -181,21 +181,18 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config - assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) + assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog) assert 'live' not in config - assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) + assert not log_has('Parameter -l/--live detected ...', caplog) assert 'position_stacking' not in config - assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + assert not log_has('Parameter --enable-position-stacking detected ...', caplog) assert 'refresh_pairs' not in config - assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog) assert 'timerange' not in config assert 'export' not in config @@ -235,43 +232,31 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert 'datadir' in config assert config['runmode'] == RunMode.BACKTEST - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', - caplog.record_tuples) + caplog) assert 'live' in config - assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) + assert log_has('Parameter -l/--live detected ...', caplog) assert 'position_stacking' in config - assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + assert log_has('Parameter --enable-position-stacking detected ...', caplog) assert 'use_max_market_positions' in config - assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) - assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) + assert log_has('Parameter --disable-max-market-positions detected ...', caplog) + assert log_has('max_open_trades set to unlimited ...', caplog) assert 'refresh_pairs' in config - assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog) assert 'timerange' in config - assert log_has( - 'Parameter --timerange detected: {} ...'.format(config['timerange']), - caplog.record_tuples - ) + assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog) assert 'export' in config - assert log_has( - 'Parameter --export detected: {} ...'.format(config['export']), - caplog.record_tuples - ) + assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) assert 'exportfilename' in config - assert log_has( - 'Storing backtest results to {} ...'.format(config['exportfilename']), - caplog.record_tuples - ) + assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog) def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: @@ -303,10 +288,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: ] args = get_args(args) start_backtesting(args) - assert log_has( - 'Starting freqtrade in Backtesting mode', - caplog.record_tuples - ) + assert log_has('Starting freqtrade in Backtesting mode', caplog) assert start_mock.call_count == 1 @@ -360,7 +342,7 @@ def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> No with pytest.raises(OperationalException): Backtesting(default_conf) log_has("Ticker-interval needs to be set in either configuration " - "or as cli argument `--ticker-interval 5m`", caplog.record_tuples) + "or as cli argument `--ticker-interval 5m`", caplog) def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None: @@ -511,7 +493,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: 'up to 2017-11-14T22:59:00+00:00 (0 days)..' ] for line in exists: - assert log_has(line, caplog.record_tuples) + assert log_has(line, caplog) def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: @@ -539,7 +521,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: backtesting.start() # check the logs, that will contain the backtest result - assert log_has('No data found. Terminating.', caplog.record_tuples) + assert log_has('No data found. Terminating.', caplog) def test_backtest(default_conf, fee, mocker) -> None: @@ -876,7 +858,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): ] for line in exists: - assert log_has(line, caplog.record_tuples) + assert log_has(line, caplog) @pytest.mark.filterwarnings("ignore:DEPRECATED") @@ -936,4 +918,4 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): ] for line in exists: - assert log_has(line, caplog.record_tuples) + assert log_has(line, caplog) diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index badaa5c45..cdc724db2 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -29,15 +29,12 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config - assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) + assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog) assert 'refresh_pairs' not in config - assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog) assert 'timerange' not in config assert 'stoploss_range' not in config @@ -69,21 +66,15 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert config['runmode'] == RunMode.EDGE - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', - caplog.record_tuples) + caplog) assert 'refresh_pairs' in config - assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog) assert 'timerange' in config - assert log_has( - 'Parameter --timerange detected: {} ...'.format(config['timerange']), - caplog.record_tuples - ) + assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog) def test_start(mocker, fee, edge_conf, caplog) -> None: @@ -100,10 +91,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: ] args = get_args(args) start_edge(args) - assert log_has( - 'Starting freqtrade in Edge mode', - caplog.record_tuples - ) + assert log_has('Starting freqtrade in Edge mode', caplog) assert start_mock.call_count == 1 diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index e3b049c06..cff1315a0 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -79,21 +79,18 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config - assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) + assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog) assert 'live' not in config - assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) + assert not log_has('Parameter -l/--live detected ...', caplog) assert 'position_stacking' not in config - assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + assert not log_has('Parameter --enable-position-stacking detected ...', caplog) assert 'refresh_pairs' not in config - assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog) assert 'timerange' not in config assert 'runmode' in config @@ -130,41 +127,32 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo assert 'datadir' in config assert config['runmode'] == RunMode.HYPEROPT - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', - caplog.record_tuples) + caplog) assert 'position_stacking' in config - assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + assert log_has('Parameter --enable-position-stacking detected ...', caplog) assert 'use_max_market_positions' in config - assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) - assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) + assert log_has('Parameter --disable-max-market-positions detected ...', caplog) + assert log_has('max_open_trades set to unlimited ...', caplog) assert 'refresh_pairs' in config - assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog) assert 'timerange' in config - assert log_has( - 'Parameter --timerange detected: {} ...'.format(config['timerange']), - caplog.record_tuples - ) + assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog) assert 'epochs' in config assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...', - caplog.record_tuples) + caplog) assert 'spaces' in config - assert log_has( - 'Parameter -s/--spaces detected: {}'.format(config['spaces']), - caplog.record_tuples - ) + assert log_has('Parameter -s/--spaces detected: {}'.format(config['spaces']), caplog) assert 'print_all' in config - assert log_has('Parameter --print-all detected ...', caplog.record_tuples) + assert log_has('Parameter --print-all detected ...', caplog) def test_hyperoptresolver(mocker, default_conf, caplog) -> None: @@ -181,9 +169,9 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_sell_trend') assert log_has("Custom Hyperopt does not provide populate_sell_trend. " - "Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples) + "Using populate_sell_trend from DefaultStrategy.", caplog) assert log_has("Custom Hyperopt does not provide populate_buy_trend. " - "Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples) + "Using populate_buy_trend from DefaultStrategy.", caplog) assert hasattr(x, "ticker_interval") @@ -229,10 +217,7 @@ def test_start(mocker, default_conf, caplog) -> None: import pprint pprint.pprint(caplog.record_tuples) - assert log_has( - 'Starting freqtrade in Hyperopt mode', - caplog.record_tuples - ) + assert log_has('Starting freqtrade in Hyperopt mode', caplog) assert start_mock.call_count == 1 @@ -257,7 +242,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: import pprint pprint.pprint(caplog.record_tuples) - assert log_has('No data found. Terminating.', caplog.record_tuples) + assert log_has('No data found. Terminating.', caplog) def test_start_failure(mocker, default_conf, caplog) -> None: @@ -275,10 +260,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None: args = get_args(args) with pytest.raises(DependencyException): start_hyperopt(args) - assert log_has( - "Please don't use --strategy for hyperopt.", - caplog.record_tuples - ) + assert log_has("Please don't use --strategy for hyperopt.", caplog) def test_start_filelock(mocker, default_conf, caplog) -> None: @@ -294,10 +276,7 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: ] args = get_args(args) start_hyperopt(args) - assert log_has( - "Another running instance of freqtrade Hyperopt detected.", - caplog.record_tuples - ) + assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog) def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: @@ -401,10 +380,7 @@ def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None: hyperopt.save_trials() trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') - assert log_has( - 'Saving 1 evaluations to \'{}\''.format(trials_file), - caplog.record_tuples - ) + assert log_has('Saving 1 evaluations to \'{}\''.format(trials_file), caplog) mock_dump.assert_called_once() @@ -413,10 +389,7 @@ def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None: mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials) hyperopt_trial = hyperopt.read_trials() trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') - assert log_has( - 'Reading Trials from \'{}\''.format(trials_file), - caplog.record_tuples - ) + assert log_has('Reading Trials from \'{}\''.format(trials_file), caplog) assert hyperopt_trial == trials mock_load.assert_called_once() @@ -626,7 +599,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog): Hyperopt(default_conf) assert unlinkmock.call_count == 2 - assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog.record_tuples) + assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog) def test_continue_hyperopt(mocker, default_conf, caplog): @@ -643,4 +616,4 @@ def test_continue_hyperopt(mocker, default_conf, caplog): Hyperopt(default_conf) assert unlinkmock.call_count == 0 - assert log_has(f"Continuing on previous hyperopt results.", caplog.record_tuples) + assert log_has(f"Continuing on previous hyperopt results.", caplog) diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 9f5ab70e3..0eb7630a1 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -49,12 +49,12 @@ def test_returns_latest_sell_signal(mocker, default_conf, ticker_history): def test_get_signal_empty(default_conf, mocker, caplog): assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], DataFrame()) - assert log_has('Empty ticker history for pair foo', caplog.record_tuples) + assert log_has('Empty ticker history for pair foo', caplog) caplog.clear() assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'], []) - assert log_has('Empty ticker history for pair bar', caplog.record_tuples) + assert log_has('Empty ticker history for pair bar', caplog) def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): @@ -65,7 +65,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_hi ) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], ticker_history) - assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples) + assert log_has('Unable to analyze ticker for pair foo: xyz', caplog) def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): @@ -76,7 +76,7 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], ticker_history) - assert log_has('Empty dataframe for pair xyz', caplog.record_tuples) + assert log_has('Empty dataframe for pair xyz', caplog) def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): @@ -91,10 +91,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], ticker_history) - assert log_has( - 'Outdated history for pair xyz. Last tick is 16 minutes old', - caplog.record_tuples - ) + assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) def test_get_signal_handles_exceptions(mocker, default_conf): @@ -237,9 +234,8 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: assert buy_mock.call_count == 1 assert buy_mock.call_count == 1 - assert log_has('TA Analysis Launched', caplog.record_tuples) - assert not log_has('Skipping TA Analysis for already analyzed candle', - caplog.record_tuples) + assert log_has('TA Analysis Launched', caplog) + assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) @@ -247,9 +243,8 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: assert ind_mock.call_count == 2 assert buy_mock.call_count == 2 assert buy_mock.call_count == 2 - assert log_has('TA Analysis Launched', caplog.record_tuples) - assert not log_has('Skipping TA Analysis for already analyzed candle', - caplog.record_tuples) + assert log_has('TA Analysis Launched', caplog) + assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None: @@ -275,9 +270,8 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) - assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 assert buy_mock.call_count == 1 - assert log_has('TA Analysis Launched', caplog.record_tuples) - assert not log_has('Skipping TA Analysis for already analyzed candle', - caplog.record_tuples) + assert log_has('TA Analysis Launched', caplog) + assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) @@ -290,6 +284,5 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) - assert 'sell' in ret.columns assert ret['buy'].sum() == 0 assert ret['sell'].sum() == 0 - assert not log_has('TA Analysis Launched', caplog.record_tuples) - assert log_has('Skipping TA Analysis for already analyzed candle', - caplog.record_tuples) + assert not log_has('TA Analysis Launched', caplog) + assert log_has('Skipping TA Analysis for already analyzed candle', caplog) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index df8c0f126..8f690193b 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -76,8 +76,7 @@ def test_load_strategy_base64(result, caplog): assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) # Make sure strategy was loaded from base64 (using temp directory)!! assert log_has_re(r"Using resolved strategy TestStrategy from '" - + tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.", - caplog.record_tuples) + + tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.", caplog) def test_load_strategy_invalid_directory(result, caplog): @@ -85,7 +84,7 @@ def test_load_strategy_invalid_directory(result, caplog): extra_dir = Path.cwd() / 'some/path' resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) - assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples) + assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) @@ -105,7 +104,7 @@ def test_load_staticmethod_importerror(mocker, caplog): match=r"Impossible to load Strategy 'DefaultStrategy'. " r"This class does not exist or contains Python code errors."): StrategyResolver() - assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples) + assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog) def test_strategy(result): From dc5719e1f4a51d054d6ff43cbd2d243474eb8220 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 20:17:22 +0200 Subject: [PATCH 122/269] Adapt rpc to new log_has method --- freqtrade/tests/rpc/test_fiat_convert.py | 4 +- freqtrade/tests/rpc/test_rpc_apiserver.py | 20 ++++----- freqtrade/tests/rpc/test_rpc_manager.py | 20 ++++----- freqtrade/tests/rpc/test_rpc_telegram.py | 52 +++++------------------ freqtrade/tests/rpc/test_rpc_webhook.py | 6 +-- 5 files changed, 35 insertions(+), 67 deletions(-) diff --git a/freqtrade/tests/rpc/test_fiat_convert.py b/freqtrade/tests/rpc/test_fiat_convert.py index 66870efcc..1689ecac6 100644 --- a/freqtrade/tests/rpc/test_fiat_convert.py +++ b/freqtrade/tests/rpc/test_fiat_convert.py @@ -91,7 +91,7 @@ def test_fiat_convert_unsupported_crypto(mocker, caplog): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[]) fiat_convert = CryptoToFiatConverter() assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0 - assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples) + assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog) def test_fiat_convert_get_price(mocker): @@ -190,7 +190,7 @@ def test_fiat_invalid_response(mocker, caplog): length_cryptomap = len(fiat_convert._cryptomap) assert length_cryptomap == 0 assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError', - caplog.record_tuples) + caplog) def test_convert_amount(mocker): diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index bd420ada6..a218d5622 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -148,8 +148,8 @@ def test_api_run(default_conf, mocker, caplog): assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert hasattr(apiserver, "srv") - assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) - assert log_has("Starting Local Rest Server.", caplog.record_tuples) + assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog) + assert log_has("Starting Local Rest Server.", caplog) # Test binding to public caplog.clear() @@ -165,22 +165,20 @@ def test_api_run(default_conf, mocker, caplog): assert server_mock.call_args_list[0][0][0] == "0.0.0.0" assert server_mock.call_args_list[0][0][1] == "8089" assert isinstance(server_mock.call_args_list[0][0][2], Flask) - assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) - assert log_has("Starting Local Rest Server.", caplog.record_tuples) + assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog) + assert log_has("Starting Local Rest Server.", caplog) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", - caplog.record_tuples) + caplog) assert log_has("SECURITY WARNING - This is insecure please set to your loopback," - "e.g 127.0.0.1 in config.json", - caplog.record_tuples) + "e.g 127.0.0.1 in config.json", caplog) assert log_has("SECURITY WARNING - No password for local REST Server defined. " - "Please make sure that this is intentional!", - caplog.record_tuples) + "Please make sure that this is intentional!", caplog) # Test crashing flask caplog.clear() mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) apiserver.run() - assert log_has("Api server failed to start.", caplog.record_tuples) + assert log_has("Api server failed to start.", caplog) def test_api_cleanup(default_conf, mocker, caplog): @@ -199,7 +197,7 @@ def test_api_cleanup(default_conf, mocker, caplog): apiserver.cleanup() assert stop_mock.shutdown.call_count == 1 - assert log_has("Stopping API Server", caplog.record_tuples) + assert log_has("Stopping API Server", caplog) def test_api_reloadconf(botclient): diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 91fd2297f..468e3e8e5 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -19,7 +19,7 @@ def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: default_conf['telegram']['enabled'] = False rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) - assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples) + assert not log_has('Enabling rpc.telegram ...', caplog) assert rpc_manager.registered_modules == [] @@ -28,7 +28,7 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) - assert log_has('Enabling rpc.telegram ...', caplog.record_tuples) + assert log_has('Enabling rpc.telegram ...', caplog) len_modules = len(rpc_manager.registered_modules) assert len_modules == 1 assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules] @@ -43,7 +43,7 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: rpc_manager = RPCManager(freqtradebot) rpc_manager.cleanup() - assert not log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) + assert not log_has('Cleaning up rpc.telegram ...', caplog) assert telegram_mock.call_count == 0 @@ -59,7 +59,7 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None: assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules] rpc_manager.cleanup() - assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) + assert log_has('Cleaning up rpc.telegram ...', caplog) assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules] assert telegram_mock.call_count == 1 @@ -75,7 +75,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: 'status': 'test' }) - assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) + assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog) assert telegram_mock.call_count == 0 @@ -90,7 +90,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: 'status': 'test' }) - assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) + assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog) assert telegram_mock.call_count == 1 @@ -100,7 +100,7 @@ def test_init_webhook_disabled(mocker, default_conf, caplog) -> None: default_conf['webhook'] = {'enabled': False} rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) - assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples) + assert not log_has('Enabling rpc.webhook ...', caplog) assert rpc_manager.registered_modules == [] @@ -110,7 +110,7 @@ def test_init_webhook_enabled(mocker, default_conf, caplog) -> None: default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"} rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) - assert log_has('Enabling rpc.webhook ...', caplog.record_tuples) + assert log_has('Enabling rpc.webhook ...', caplog) assert len(rpc_manager.registered_modules) == 1 assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] @@ -144,7 +144,7 @@ def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: default_conf['telegram']['enabled'] = False rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) - assert not log_has('Enabling rpc.api_server', caplog.record_tuples) + assert not log_has('Enabling rpc.api_server', caplog) assert rpc_manager.registered_modules == [] assert run_mock.call_count == 0 @@ -160,7 +160,7 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: "listen_port": "8080"} rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) - assert log_has('Enabling rpc.api_server', caplog.record_tuples) + assert log_has('Enabling rpc.api_server', caplog) assert len(rpc_manager.registered_modules) == 1 assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules] assert run_mock.call_count == 1 diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 1bee5bff3..3575520ad 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -76,7 +76,7 @@ def test_init(default_conf, mocker, caplog) -> None: "['performance'], ['daily'], ['count'], ['reload_conf'], " \ "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]" - assert log_has(message_str, caplog.record_tuples) + assert log_has(message_str, caplog) def test_cleanup(default_conf, mocker) -> None: @@ -102,18 +102,9 @@ def test_authorized_only(default_conf, mocker, caplog) -> None: dummy = DummyCls(bot) dummy.dummy_handler(bot=MagicMock(), update=update) assert dummy.state['called'] is True - assert log_has( - 'Executing handler: dummy_handler for chat_id: 0', - caplog.record_tuples - ) - assert not log_has( - 'Rejected unauthorized message from: 0', - caplog.record_tuples - ) - assert not log_has( - 'Exception occurred within Telegram module', - caplog.record_tuples - ) + assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog) + assert not log_has('Rejected unauthorized message from: 0', caplog) + assert not log_has('Exception occurred within Telegram module', caplog) def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: @@ -128,18 +119,9 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: dummy = DummyCls(bot) dummy.dummy_handler(bot=MagicMock(), update=update) assert dummy.state['called'] is False - assert not log_has( - 'Executing handler: dummy_handler for chat_id: 3735928559', - caplog.record_tuples - ) - assert log_has( - 'Rejected unauthorized message from: 3735928559', - caplog.record_tuples - ) - assert not log_has( - 'Exception occurred within Telegram module', - caplog.record_tuples - ) + assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog) + assert log_has('Rejected unauthorized message from: 3735928559', caplog) + assert not log_has('Exception occurred within Telegram module', caplog) def test_authorized_only_exception(default_conf, mocker, caplog) -> None: @@ -156,18 +138,9 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: dummy.dummy_exception(bot=MagicMock(), update=update) assert dummy.state['called'] is False - assert not log_has( - 'Executing handler: dummy_handler for chat_id: 0', - caplog.record_tuples - ) - assert not log_has( - 'Rejected unauthorized message from: 0', - caplog.record_tuples - ) - assert log_has( - 'Exception occurred within Telegram module', - caplog.record_tuples - ) + assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog) + assert not log_has('Rejected unauthorized message from: 0', caplog) + assert log_has('Exception occurred within Telegram module', caplog) def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: @@ -1440,7 +1413,4 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: # Bot should've tried to send it twice assert len(bot.method_calls) == 2 - assert log_has( - 'Telegram NetworkError: Oh snap! Trying one more time.', - caplog.record_tuples - ) + assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog) diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py index a2dcd9b31..cc491d4dd 100644 --- a/freqtrade/tests/rpc/test_rpc_webhook.py +++ b/freqtrade/tests/rpc/test_rpc_webhook.py @@ -115,7 +115,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks", - caplog.record_tuples) + caplog) default_conf["webhook"] = get_webhook_dict() default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}" @@ -135,7 +135,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): } webhook.send_msg(msg) assert log_has("Problem calling Webhook. Please check your webhook configuration. " - "Exception: 'DEADBEEF'", caplog.record_tuples) + "Exception: 'DEADBEEF'", caplog) msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) @@ -164,4 +164,4 @@ def test__send_msg(default_conf, mocker, caplog): post = MagicMock(side_effect=RequestException) mocker.patch("freqtrade.rpc.webhook.post", post) webhook._send_msg(msg) - assert log_has('Could not call webhook url. Exception: ', caplog.record_tuples) + assert log_has('Could not call webhook url. Exception: ', caplog) From a636dda07d20addd1b4f80df943c3701d5192cd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 20:17:39 +0200 Subject: [PATCH 123/269] Fix remaining tests using log_has --- freqtrade/tests/exchange/test_exchange.py | 45 +++++------ freqtrade/tests/test_configuration.py | 91 +++++++++-------------- freqtrade/tests/test_freqtradebot.py | 86 ++++++++++----------- freqtrade/tests/test_main.py | 14 ++-- freqtrade/tests/test_persistence.py | 20 +++-- freqtrade/tests/test_plotting.py | 10 +-- 6 files changed, 114 insertions(+), 152 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index ebe5ad9df..e8a7201f1 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -62,7 +62,7 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu def test_init(default_conf, mocker, caplog): caplog.set_level(logging.INFO) get_patched_exchange(mocker, default_conf) - assert log_has('Instance is running with dry_run enabled', caplog.record_tuples) + assert log_has('Instance is running with dry_run enabled', caplog) def test_init_ccxt_kwargs(default_conf, mocker, caplog): @@ -71,8 +71,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): conf = copy.deepcopy(default_conf) conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True} ex = Exchange(conf) - assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", - caplog.record_tuples) + assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog) assert ex._api_async.aiohttp_trust_env assert not ex._api.aiohttp_trust_env @@ -81,20 +80,18 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): conf = copy.deepcopy(default_conf) conf['exchange']['ccxt_config'] = {'TestKWARG': 11} ex = Exchange(conf) - assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", - caplog.record_tuples) + assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog) assert not ex._api_async.aiohttp_trust_env assert hasattr(ex._api, 'TestKWARG') assert ex._api.TestKWARG == 11 assert not hasattr(ex._api_async, 'TestKWARG') - assert log_has("Applying additional ccxt config: {'TestKWARG': 11}", - caplog.record_tuples) + assert log_has("Applying additional ccxt config: {'TestKWARG': 11}", caplog) def test_destroy(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) get_patched_exchange(mocker, default_conf) - assert log_has('Exchange object destroyed, closing async loop', caplog.record_tuples) + assert log_has('Exchange object destroyed, closing async loop', caplog) def test_init_exception(default_conf, mocker): @@ -120,8 +117,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) exchange = ExchangeResolver('Bittrex', default_conf).exchange assert isinstance(exchange, Exchange) - assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", - caplog.record_tuples) + assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) caplog.clear() exchange = ExchangeResolver('kraken', default_conf).exchange @@ -129,7 +125,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): assert isinstance(exchange, Kraken) assert not isinstance(exchange, Binance) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", - caplog.record_tuples) + caplog) exchange = ExchangeResolver('binance', default_conf).exchange assert isinstance(exchange, Exchange) @@ -137,7 +133,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): assert not isinstance(exchange, Kraken) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", - caplog.record_tuples) + caplog) def test_validate_order_time_in_force(default_conf, mocker, caplog): @@ -249,8 +245,7 @@ def test__load_async_markets(default_conf, mocker, caplog): exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) exchange._load_async_markets() - assert log_has('Could not load async markets. Reason: deadbeef', - caplog.record_tuples) + assert log_has('Could not load async markets. Reason: deadbeef', caplog) def test__load_markets(default_conf, mocker, caplog): @@ -262,7 +257,7 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) Exchange(default_conf) - assert log_has('Unable to initialize markets. Reason: SomeError', caplog.record_tuples) + assert log_has('Unable to initialize markets. Reason: SomeError', caplog) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() @@ -298,7 +293,7 @@ def test__reload_markets(default_conf, mocker, caplog): exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60 exchange._reload_markets() assert exchange.markets == updated_markets - assert log_has('Performing scheduled market reload..', caplog.record_tuples) + assert log_has('Performing scheduled market reload..', caplog) def test__reload_markets_exception(default_conf, mocker, caplog): @@ -312,7 +307,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog): # less than 10 minutes have passed, no reload exchange._reload_markets() assert exchange._last_markets_refresh == 0 - assert log_has_re(r"Could not reload markets.*", caplog.record_tuples) + assert log_has_re(r"Could not reload markets.*", caplog) def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly @@ -357,8 +352,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) Exchange(default_conf) - assert log_has('Unable to validate pairs (assuming they are correct).', - caplog.record_tuples) + assert log_has('Unable to validate pairs (assuming they are correct).', caplog) def test_validate_pairs_restricted(default_conf, mocker, caplog): @@ -374,8 +368,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): Exchange(default_conf) assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange." f"Please check if you are impacted by this restriction " - f"on the exchange and eventually remove XRP/BTC from your whitelist.", - caplog.record_tuples) + f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog) def test_validate_timeframes(default_conf, mocker): @@ -1060,7 +1053,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: assert not exchange._klines exchange.refresh_latest_ohlcv(pairs) - assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog.record_tuples) + assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog) assert exchange._klines assert exchange._api_async.fetch_ohlcv.call_count == 2 for pair in pairs: @@ -1079,7 +1072,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: assert exchange._api_async.fetch_ohlcv.call_count == 2 assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...", - caplog.record_tuples) + caplog) @pytest.mark.asyncio @@ -1109,7 +1102,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ assert res[1] == "5m" assert res[2] == tick assert exchange._api_async.fetch_ohlcv.call_count == 1 - assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog.record_tuples) + assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog) # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), @@ -1168,8 +1161,8 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): # Test that each is in list at least once as order is not guaranteed assert type(res[0]) is tuple or type(res[1]) is tuple assert type(res[0]) is TypeError or type(res[1]) is TypeError - assert log_has("Error loading ETH/BTC. Result was [[]].", caplog.record_tuples) - assert log_has("Async code raised an exception: TypeError", caplog.record_tuples) + assert log_has("Error loading ETH/BTC. Result was [[]].", caplog) + assert log_has("Async code raised an exception: TypeError", caplog) @pytest.mark.parametrize("exchange_name", EXCHANGES) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index e325a0de2..55df3acf7 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -72,7 +72,7 @@ def test__args_to_config(caplog): # No warnings ... configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef") assert len(w) == 0 - assert log_has("DeadBeef", caplog.record_tuples) + assert log_has("DeadBeef", caplog) assert config['strategy_path'] == "TestTest" configuration = Configuration(args) @@ -84,7 +84,7 @@ def test__args_to_config(caplog): assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "DEPRECATED: Going away soon!" in str(w[-1].message) - assert log_has("DeadBeef", caplog.record_tuples) + assert log_has("DeadBeef", caplog) assert config['strategy_path'] == "TestTest" @@ -98,7 +98,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: assert validated_conf['max_open_trades'] == 0 assert 'internals' in validated_conf - assert log_has('Validating configuration ...', caplog.record_tuples) + assert log_has('Validating configuration ...', caplog) def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: @@ -130,7 +130,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist'] assert 'internals' in validated_conf - assert log_has('Validating configuration ...', caplog.record_tuples) + assert log_has('Validating configuration ...', caplog) def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: @@ -143,7 +143,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> assert validated_conf['max_open_trades'] > 999999999 assert validated_conf['max_open_trades'] == float('inf') - assert log_has('Validating configuration ...', caplog.record_tuples) + assert log_has('Validating configuration ...', caplog) assert "runmode" in validated_conf assert validated_conf['runmode'] == RunMode.DRY_RUN @@ -280,8 +280,8 @@ def test_show_info(default_conf, mocker, caplog) -> None: configuration = Configuration(args) configuration.get_config() - assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples) - assert log_has('Dry run is enabled', caplog.record_tuples) + assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog) + assert log_has('Dry run is enabled', caplog) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: @@ -303,21 +303,18 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config - assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) + assert not log_has('Parameter -i/--ticker-interval detected ...', caplog) assert 'live' not in config - assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) + assert not log_has('Parameter -l/--live detected ...', caplog) assert 'position_stacking' not in config - assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + assert not log_has('Parameter --enable-position-stacking detected ...', caplog) assert 'refresh_pairs' not in config - assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog) assert 'timerange' not in config assert 'export' not in config @@ -355,37 +352,28 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', - caplog.record_tuples) + caplog) assert 'live' in config - assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) + assert log_has('Parameter -l/--live detected ...', caplog) assert 'position_stacking'in config - assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + assert log_has('Parameter --enable-position-stacking detected ...', caplog) assert 'use_max_market_positions' in config - assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) - assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) + assert log_has('Parameter --disable-max-market-positions detected ...', caplog) + assert log_has('max_open_trades set to unlimited ...', caplog) assert 'refresh_pairs'in config - assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog) assert 'timerange' in config - assert log_has( - 'Parameter --timerange detected: {} ...'.format(config['timerange']), - caplog.record_tuples - ) + assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog) assert 'export' in config - assert log_has( - 'Parameter --export detected: {} ...'.format(config['export']), - caplog.record_tuples - ) + assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: @@ -415,16 +403,13 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert log_has( - 'Using data directory: {} ...'.format(config['datadir']), - caplog.record_tuples - ) + assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'ticker_interval' in config assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', - caplog.record_tuples) + caplog) assert 'strategy_list' in config - assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples) + assert log_has('Using strategy list of 2 Strategies', caplog) assert 'position_stacking' not in config @@ -433,10 +418,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non assert 'timerange' not in config assert 'export' in config - assert log_has( - 'Parameter --export detected: {} ...'.format(config['export']), - caplog.record_tuples - ) + assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: @@ -455,11 +437,11 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: assert 'epochs' in config assert int(config['epochs']) == 10 assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...', - caplog.record_tuples) + caplog) assert 'spaces' in config assert config['spaces'] == ['all'] - assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples) + assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog) assert "runmode" in config assert config['runmode'] == RunMode.HYPEROPT @@ -469,38 +451,35 @@ def test_check_exchange(default_conf, caplog) -> None: default_conf.get('exchange').update({'name': 'BITTREX'}) assert check_exchange(default_conf) assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", - caplog.record_tuples) + caplog) caplog.clear() # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'binance'}) assert check_exchange(default_conf) assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", - caplog.record_tuples) + caplog) caplog.clear() # Test an available exchange, supported by ccxt default_conf.get('exchange').update({'name': 'kraken'}) assert check_exchange(default_conf) assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " - r"by the Freqtrade development team\. .*", - caplog.record_tuples) + r"by the Freqtrade development team\. .*", caplog) caplog.clear() # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) assert not check_exchange(default_conf) assert log_has_re(r"Exchange .* is known to not work with the bot yet\. " - r"Use it only for development and testing purposes\.", - caplog.record_tuples) + r"Use it only for development and testing purposes\.", caplog) caplog.clear() # Test a 'bad' exchange with check_for_bad=False default_conf.get('exchange').update({'name': 'bitmex'}) assert check_exchange(default_conf, False) assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " - r"by the Freqtrade development team\. .*", - caplog.record_tuples) + r"by the Freqtrade development team\. .*", caplog) caplog.clear() # Test an invalid exchange @@ -526,7 +505,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: validated_conf = configuration.load_config() assert validated_conf.get('verbosity') == 3 - assert log_has('Verbosity set to 3', caplog.record_tuples) + assert log_has('Verbosity set to 3', caplog) def test_set_loggers() -> None: @@ -592,7 +571,7 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: validated_conf = configuration.load_config() assert validated_conf.get('forcebuy_enable') - assert log_has('`forcebuy` RPC message enabled.', caplog.record_tuples) + assert log_has('`forcebuy` RPC message enabled.', caplog) def test_validate_default_conf(default_conf) -> None: @@ -605,7 +584,7 @@ def test_create_datadir(mocker, default_conf, caplog) -> None: create_datadir(default_conf, '/foo/bar') assert md.call_args[1]['parents'] is True - assert log_has('Created data directory: /foo/bar', caplog.record_tuples) + assert log_has('Created data directory: /foo/bar', caplog) def test_validate_tsl(default_conf): diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 1a4c5159c..cf9a4fa29 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -63,7 +63,7 @@ def test_cleanup(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.persistence.cleanup', mock_cleanup) freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.cleanup() - assert log_has('Cleaning up modules ...', caplog.record_tuples) + assert log_has('Cleaning up modules ...', caplog) assert mock_cleanup.call_count == 1 @@ -76,7 +76,7 @@ def test_worker_running(mocker, default_conf, caplog) -> None: state = worker._worker(old_state=None) assert state is State.RUNNING - assert log_has('Changing state to: RUNNING', caplog.record_tuples) + assert log_has('Changing state to: RUNNING', caplog) assert mock_throttle.call_count == 1 # Check strategy is loaded, and received a dataprovider object assert worker.freqtrade.strategy @@ -93,7 +93,7 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None: worker.state = State.STOPPED state = worker._worker(old_state=State.RUNNING) assert state is State.STOPPED - assert log_has('Changing state to: STOPPED', caplog.record_tuples) + assert log_has('Changing state to: STOPPED', caplog) assert mock_throttle.call_count == 0 assert mock_sleep.call_count == 1 @@ -111,7 +111,7 @@ def test_throttle(mocker, default_conf, caplog) -> None: assert result == 42 assert end - start > 0.1 - assert log_has('Throttling throttled_func for 0.10 seconds', caplog.record_tuples) + assert log_has('Throttling throttled_func for 0.10 seconds', caplog) result = worker._throttle(throttled_func, min_secs=-1) assert result == 42 @@ -262,7 +262,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, # stoploss shoud be hit assert freqtrade.handle_trade(trade) is True - assert log_has('executed sell, reason: SellType.STOP_LOSS', caplog.record_tuples) + assert log_has('executed sell, reason: SellType.STOP_LOSS', caplog) assert trade.sell_reason == SellType.STOP_LOSS.value @@ -582,8 +582,7 @@ def test_create_trade_no_pairs_let(default_conf, ticker, limit_buy_order, fee, assert freqtrade.create_trade() assert not freqtrade.create_trade() - assert log_has("No currency pair in whitelist, but checking to sell open trades.", - caplog.record_tuples) + assert log_has("No currency pair in whitelist, but checking to sell open trades.", caplog) def test_create_trade_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, @@ -602,7 +601,7 @@ def test_create_trade_no_pairs_in_whitelist(default_conf, ticker, limit_buy_orde patch_get_signal(freqtrade) assert not freqtrade.create_trade() - assert log_has("Whitelist is empty.", caplog.record_tuples) + assert log_has("Whitelist is empty.", caplog) def test_create_trade_no_signal(default_conf, fee, mocker) -> None: @@ -657,8 +656,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, assert trade.amount == 90.99181073703367 assert log_has( - 'Buy signal found: about create a new trade with stake_amount: 0.001 ...', - caplog.record_tuples + 'Buy signal found: about create a new trade with stake_amount: 0.001 ...', caplog ) @@ -1025,7 +1023,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, }) mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True - assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog.record_tuples) + assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) assert trade.stoploss_order_id is None assert trade.is_open is False @@ -1034,7 +1032,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, side_effect=DependencyException() ) freqtrade.handle_stoploss_on_exchange(trade) - assert log_has('Unable to place a stoploss order on exchange: ', caplog.record_tuples) + assert log_has('Unable to place a stoploss order on exchange: ', caplog) # Fifth case: get_order returns InvalidOrder # It should try to add stoploss order @@ -1196,8 +1194,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) - assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", - caplog.record_tuples) + assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) # Still try to create order assert stoploss_limit.call_count == 1 @@ -1208,8 +1205,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 - assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*", - caplog.record_tuples) + assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*", caplog) def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, @@ -1340,7 +1336,7 @@ def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> No MagicMock(side_effect=DependencyException) ) freqtrade.process_maybe_execute_buy() - log_has('Unable to create trade:', caplog.record_tuples) + assert log_has('Unable to create trade: ', caplog) def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplog) -> None: @@ -1358,8 +1354,7 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo assert not freqtrade.process_maybe_execute_sell(trade) # Test amount not modified by fee-logic assert not log_has( - 'Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(trade), - caplog.record_tuples + 'Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(trade), caplog ) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) @@ -1382,7 +1377,7 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf, side_effect=DependencyException() ) freqtrade.process_maybe_execute_sell(trade) - assert log_has('Unable to sell trade: ', caplog.record_tuples) + assert log_has('Unable to sell trade: ', caplog) def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None: @@ -1401,7 +1396,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No trade.open_fee = 0.001 freqtrade.update_trade_state(trade) # Test amount not modified by fee-logic - assert not log_has_re(r'Applying fee to .*', caplog.record_tuples) + assert not log_has_re(r'Applying fee to .*', caplog) assert trade.open_order_id is None assert trade.amount == limit_buy_order['amount'] @@ -1418,7 +1413,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No # Assert we call handle_trade() if trade is feasible for execution freqtrade.update_trade_state(trade) - assert log_has_re('Found open order for.*', caplog.record_tuples) + assert log_has_re('Found open order for.*', caplog) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker): @@ -1457,7 +1452,7 @@ def test_update_trade_state_exception(mocker, default_conf, side_effect=OperationalException() ) freqtrade.update_trade_state(trade) - assert log_has('Could not update trade amount: ', caplog.record_tuples) + assert log_has('Could not update trade amount: ', caplog) def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None: @@ -1473,7 +1468,7 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock()) freqtrade.update_trade_state(trade) assert grm_mock.call_count == 0 - assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog.record_tuples) + assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog) def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): @@ -1632,7 +1627,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, # if ROI is reached we must sell patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) - assert log_has('Required profit reached. Selling..', caplog.record_tuples) + assert log_has('Required profit reached. Selling..', caplog) def test_handle_trade_experimental( @@ -1662,7 +1657,7 @@ def test_handle_trade_experimental( patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) - assert log_has('Sell signal received. Selling..', caplog.record_tuples) + assert log_has('Sell signal received. Selling..', caplog) def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, @@ -1768,7 +1763,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 0 - assert log_has_re("Buy order canceled on Exchange for Trade.*", caplog.record_tuples) + assert log_has_re("Buy order canceled on Exchange for Trade.*", caplog) def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, @@ -1881,7 +1876,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 assert trade_sell.is_open is True - assert log_has_re("Sell order canceled on exchange for Trade.*", caplog.record_tuples) + assert log_has_re("Sell order canceled on exchange for Trade.*", caplog) def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, @@ -1959,7 +1954,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - freqtrade.check_handle_timedout() assert log_has_re(r'Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, ' r'open_rate=0.00001099, open_since=10 hours ago\) due to Traceback \(most ' - r'recent call last\):\n.*', caplog.record_tuples) + r'recent call last\):\n.*', caplog) def test_handle_timedout_limit_buy(mocker, default_conf) -> None: @@ -2183,7 +2178,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, freqtrade.execute_sell(trade=trade, limit=1234, sell_reason=SellType.STOP_LOSS) assert sellmock.call_count == 1 - assert log_has('Could not cancel stoploss order abcd', caplog.record_tuples) + assert log_has('Could not cancel stoploss order abcd', caplog) def test_execute_sell_with_stoploss_on_exchange(default_conf, @@ -2609,7 +2604,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, assert freqtrade.handle_trade(trade) is True assert log_has( f'HIT STOP: current price at 0.000012, stop loss is 0.000015, ' - f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) + f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog) assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value @@ -2651,9 +2646,8 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets })) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss: 0.01 offset: 0 profit: 0.2666%', - caplog.record_tuples) - assert log_has(f'adjusted stop loss', caplog.record_tuples) + assert log_has(f'using positive stop loss: 0.01 offset: 0 profit: 0.2666%', caplog) + assert log_has(f'adjusted stop loss', caplog) assert trade.stop_loss == 0.0000138501 mocker.patch('freqtrade.exchange.Exchange.get_ticker', @@ -2667,7 +2661,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets assert log_has( f'HIT STOP: current price at {buy_price + 0.000002:.6f}, ' f'stop loss is {trade.stop_loss:.6f}, ' - f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) + f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog) def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, @@ -2710,9 +2704,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, })) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss: 0.01 offset: 0.011 profit: 0.2666%', - caplog.record_tuples) - assert log_has(f'adjusted stop loss', caplog.record_tuples) + assert log_has(f'using positive stop loss: 0.01 offset: 0.011 profit: 0.2666%', caplog) + assert log_has(f'adjusted stop loss', caplog) assert trade.stop_loss == 0.0000138501 mocker.patch('freqtrade.exchange.Exchange.get_ticker', @@ -2726,7 +2719,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, assert log_has( f'HIT STOP: current price at {buy_price + 0.000002:.6f}, ' f'stop loss is {trade.stop_loss:.6f}, ' - f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) + f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog) assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value @@ -2777,7 +2770,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, # stop-loss should not be adjusted as offset is not reached yet assert freqtrade.handle_trade(trade) is False - assert not log_has(f'adjusted stop loss', caplog.record_tuples) + assert not log_has(f'adjusted stop loss', caplog) assert trade.stop_loss == 0.0000098910 # price rises above the offset (rises 12% when the offset is 5.5%) @@ -2789,9 +2782,8 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, })) assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss: 0.05 offset: 0.055 profit: 0.1218%', - caplog.record_tuples) - assert log_has(f'adjusted stop loss', caplog.record_tuples) + assert log_has(f'using positive stop loss: 0.05 offset: 0.055 profit: 0.1218%', caplog) + assert log_has(f'adjusted stop loss', caplog) assert trade.stop_loss == 0.0000117705 @@ -2850,7 +2842,7 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001) assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992) from Trades', - caplog.record_tuples) + caplog) def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): @@ -2873,7 +2865,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): assert freqtrade.get_real_amount(trade, buy_order_fee) == amount assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' 'open_rate=0.24544100, open_since=closed) failed: myTrade-Dict empty found', - caplog.record_tuples) + caplog) def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker): @@ -2962,7 +2954,7 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001) assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992) from Trades', - caplog.record_tuples) + caplog) def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, caplog, mocker): @@ -2988,7 +2980,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004 assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996) from Order', - caplog.record_tuples) + caplog) def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, mocker): diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index bcaad4d7d..d8ec532b0 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -60,8 +60,8 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): main(args) - assert log_has('Using config: config.json.example ...', caplog.record_tuples) - assert log_has('Fatal exception!', caplog.record_tuples) + assert log_has('Using config: config.json.example ...', caplog) + assert log_has('Fatal exception!', caplog) def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: @@ -77,8 +77,8 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): main(args) - assert log_has('Using config: config.json.example ...', caplog.record_tuples) - assert log_has('SIGINT received, aborting ...', caplog.record_tuples) + assert log_has('Using config: config.json.example ...', caplog) + assert log_has('SIGINT received, aborting ...', caplog) def test_main_operational_exception(mocker, default_conf, caplog) -> None: @@ -97,8 +97,8 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): main(args) - assert log_has('Using config: config.json.example ...', caplog.record_tuples) - assert log_has('Oh snap!', caplog.record_tuples) + assert log_has('Using config: config.json.example ...', caplog) + assert log_has('Oh snap!', caplog) def test_main_reload_conf(mocker, default_conf, caplog) -> None: @@ -121,7 +121,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: with pytest.raises(SystemExit): main(['-c', 'config.json.example']) - assert log_has('Using config: config.json.example ...', caplog.record_tuples) + assert log_has('Using config: config.json.example ...', caplog) assert worker_mock.call_count == 4 assert reconfigure_mock.call_count == 1 assert isinstance(worker.freqtrade, FreqtradeBot) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 32425ef7b..c3ab7c128 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -151,7 +151,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): assert trade.close_date is None assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, " "pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).", - caplog.record_tuples) + caplog) caplog.clear() trade.open_order_id = 'something' @@ -162,7 +162,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): assert trade.close_date is not None assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, " "pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).", - caplog.record_tuples) + caplog) @pytest.mark.usefixtures("init_persistence") @@ -184,7 +184,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): assert trade.close_date is None assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, " "pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).", - caplog.record_tuples) + caplog) caplog.clear() trade.open_order_id = 'something' @@ -195,7 +195,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): assert trade.close_date is not None assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, " "pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).", - caplog.record_tuples) + caplog) @pytest.mark.usefixtures("init_persistence") @@ -558,10 +558,9 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.ticker_interval is None assert trade.stoploss_order_id is None assert trade.stoploss_last_update is None - assert log_has("trying trades_bak1", caplog.record_tuples) - assert log_has("trying trades_bak2", caplog.record_tuples) - assert log_has("Running database migration - backup available as trades_bak2", - caplog.record_tuples) + assert log_has("trying trades_bak1", caplog) + assert log_has("trying trades_bak2", caplog) + assert log_has("Running database migration - backup available as trades_bak2", caplog) def test_migrate_mid_state(mocker, default_conf, fee, caplog): @@ -621,9 +620,8 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 - assert log_has("trying trades_bak0", caplog.record_tuples) - assert log_has("Running database migration - backup available as trades_bak0", - caplog.record_tuples) + assert log_has("trying trades_bak0", caplog) + assert log_has("Running database migration - backup available as trades_bak0", caplog) def test_adjust_stop_loss(fee): diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index f9da773fe..bfdd72215 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -87,7 +87,7 @@ def test_add_indicators(default_conf, caplog): # No indicator found fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) assert fig == fig3 - assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples) + assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) def test_plot_trades(caplog): @@ -95,7 +95,7 @@ def test_plot_trades(caplog): # nothing happens when no trades are available fig = plot_trades(fig1, None) assert fig == fig1 - assert log_has("No trades found.", caplog.record_tuples) + assert log_has("No trades found.", caplog) pair = "ADA/BTC" filename = history.make_testdata_path(None) / "backtest-result_test.json" trades = load_backtest_data(filename) @@ -150,8 +150,8 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, c assert row_mock.call_count == 2 assert trades_mock.call_count == 1 - assert log_has("No buy-signals found.", caplog.record_tuples) - assert log_has("No sell-signals found.", caplog.record_tuples) + assert log_has("No buy-signals found.", caplog) + assert log_has("No sell-signals found.", caplog) def test_generate_candlestick_graph_no_trades(default_conf, mocker): @@ -216,7 +216,7 @@ def test_generate_plot_file(mocker, caplog): assert (plot_mock.call_args_list[0][1]['filename'] == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") assert log_has("Stored plot as user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html", - caplog.record_tuples) + caplog) def test_add_profit(): From b77c0d28131fb4e14a1606c23dc81a51b0e53a69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 20:22:50 +0200 Subject: [PATCH 124/269] Replace all "logentry" in caplog_record_tuples use log_has to have checking log-entries standardized. --- freqtrade/tests/strategy/test_strategy.py | 75 +++++++---------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 8f690193b..6c2f6f78b 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -15,7 +15,7 @@ from freqtrade.resolvers import StrategyResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import IStrategy -from freqtrade.tests.conftest import log_has_re +from freqtrade.tests.conftest import log_has, log_has_re def test_import_strategy(caplog): @@ -35,12 +35,8 @@ def test_import_strategy(caplog): assert imported_strategy.__module__ == 'freqtrade.strategy' assert imported_strategy.some_method() == 42 - assert ( - 'freqtrade.strategy', - logging.DEBUG, - 'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy ' - 'as freqtrade.strategy.DefaultStrategy', - ) in caplog.record_tuples + assert log_has('Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy ' + 'as freqtrade.strategy.DefaultStrategy', caplog) def test_search_strategy(): @@ -142,10 +138,7 @@ def test_strategy_override_minimal_roi(caplog): resolver = StrategyResolver(config) assert resolver.strategy.minimal_roi[0] == 0.5 - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'minimal_roi' with value in config file: {'0': 0.5}." - ) in caplog.record_tuples + assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog) def test_strategy_override_stoploss(caplog): @@ -157,10 +150,7 @@ def test_strategy_override_stoploss(caplog): resolver = StrategyResolver(config) assert resolver.strategy.stoploss == -0.5 - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'stoploss' with value in config file: -0.5." - ) in caplog.record_tuples + assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog) def test_strategy_override_trailing_stop(caplog): @@ -173,10 +163,7 @@ def test_strategy_override_trailing_stop(caplog): assert resolver.strategy.trailing_stop assert isinstance(resolver.strategy.trailing_stop, bool) - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'trailing_stop' with value in config file: True." - ) in caplog.record_tuples + assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog) def test_strategy_override_trailing_stop_positive(caplog): @@ -190,16 +177,12 @@ def test_strategy_override_trailing_stop_positive(caplog): resolver = StrategyResolver(config) assert resolver.strategy.trailing_stop_positive == -0.1 - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'trailing_stop_positive' with value in config file: -0.1." - ) in caplog.record_tuples + assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.", + caplog) assert resolver.strategy.trailing_stop_positive_offset == -0.2 - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'trailing_stop_positive' with value in config file: -0.1." - ) in caplog.record_tuples + assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.", + caplog) def test_strategy_override_ticker_interval(caplog): @@ -214,10 +197,8 @@ def test_strategy_override_ticker_interval(caplog): assert resolver.strategy.ticker_interval == 60 assert resolver.strategy.stake_currency == 'ETH' - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'ticker_interval' with value in config file: 60." - ) in caplog.record_tuples + assert log_has("Override strategy 'ticker_interval' with value in config file: 60.", + caplog) def test_strategy_override_process_only_new_candles(caplog): @@ -230,10 +211,8 @@ def test_strategy_override_process_only_new_candles(caplog): resolver = StrategyResolver(config) assert resolver.strategy.process_only_new_candles - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'process_only_new_candles' with value in config file: True." - ) in caplog.record_tuples + assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.", + caplog) def test_strategy_override_order_types(caplog): @@ -256,12 +235,9 @@ def test_strategy_override_order_types(caplog): for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: assert resolver.strategy.order_types[method] == order_types[method] - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'order_types' with value in config file:" - " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," - " 'stoploss_on_exchange': True}." - ) in caplog.record_tuples + assert log_has("Override strategy 'order_types' with value in config file:" + " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," + " 'stoploss_on_exchange': True}.", caplog) config = { 'strategy': 'DefaultStrategy', @@ -292,11 +268,8 @@ def test_strategy_override_order_tif(caplog): for method in ['buy', 'sell']: assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method] - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'order_time_in_force' with value in config file:" - " {'buy': 'fok', 'sell': 'gtc'}." - ) in caplog.record_tuples + assert log_has("Override strategy 'order_time_in_force' with value in config file:" + " {'buy': 'fok', 'sell': 'gtc'}.", caplog) config = { 'strategy': 'DefaultStrategy', @@ -331,10 +304,7 @@ def test_strategy_override_use_sell_signal(caplog): assert resolver.strategy.use_sell_signal assert isinstance(resolver.strategy.use_sell_signal, bool) - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'use_sell_signal' with value in config file: True." - ) in caplog.record_tuples + assert log_has("Override strategy 'use_sell_signal' with value in config file: True.", caplog) def test_strategy_override_use_sell_profit_only(caplog): @@ -359,10 +329,7 @@ def test_strategy_override_use_sell_profit_only(caplog): assert resolver.strategy.sell_profit_only assert isinstance(resolver.strategy.sell_profit_only, bool) - assert ('freqtrade.resolvers.strategy_resolver', - logging.INFO, - "Override strategy 'sell_profit_only' with value in config file: True." - ) in caplog.record_tuples + assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog) @pytest.mark.filterwarnings("ignore:deprecated") From c5d8499ad2c70847975f96393cfaf242d75640e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Aug 2019 20:30:15 +0200 Subject: [PATCH 125/269] Improve documentation regarding tests --- docs/developer.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index 28369bb73..259bfafd8 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -17,6 +17,29 @@ Alternatively (if your system is not supported by the setup.sh script), follow t This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. +### Tests + +New code should be covered by basic unittests. Depending on the complexity of the feature, Reviewers may request more in-depth unittests. +If necessary, the Freqtrade team can assist and give guidance with writing good tests (however please don't expect anyone to write the tests for you). + +#### Checking log content in tests + +Freqtrade uses 2 main methods to check log content in tests, `log_has()` and `log_has_re()` (to check using regex, in case of dynamic log-messages). +These are available from `conftest.py` and can be imported in any test module. + +A sample check looks as follows: + +``` python +from freqtrade.tests.conftest import log_has, log_has_re + +def test_method_to_test(caplog): + method_to_test() + + assert log_has("This event happened", caplog) + # Check regex with trailing number ... + assert log_has_re(r"This dynamic event happened and produced \d+", caplog) +``` + ## Modules ### Dynamic Pairlist From 2d60e4b18b2c982c88578cdd6ff81f0c6b993396 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 12 Aug 2019 00:32:03 +0300 Subject: [PATCH 126/269] allow comments and trailing commas in config files --- freqtrade/configuration/load_config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 25504144f..7a3ca1798 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -1,7 +1,7 @@ """ This module contain functions to load the configuration file """ -import json +import rapidjson import logging import sys from typing import Any, Dict @@ -12,6 +12,9 @@ from freqtrade import OperationalException logger = logging.getLogger(__name__) +CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS + + def load_config_file(path: str) -> Dict[str, Any]: """ Loads a config file from the given path @@ -21,7 +24,7 @@ def load_config_file(path: str) -> Dict[str, Any]: try: # Read config from stdin if requested in the options with open(path) if path != '-' else sys.stdin as file: - config = json.load(file) + config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) except FileNotFoundError: raise OperationalException( f'Config file "{path}" not found!' From 90b75afdb1373ee3c38eeeb495f053d4f3306abd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 12 Aug 2019 00:33:34 +0300 Subject: [PATCH 127/269] test added to load config with comments and trailing commas --- freqtrade/tests/config_test_comments.json | 133 ++++++++++++++++++++++ freqtrade/tests/test_configuration.py | 11 ++ 2 files changed, 144 insertions(+) create mode 100644 freqtrade/tests/config_test_comments.json diff --git a/freqtrade/tests/config_test_comments.json b/freqtrade/tests/config_test_comments.json new file mode 100644 index 000000000..85becc3e8 --- /dev/null +++ b/freqtrade/tests/config_test_comments.json @@ -0,0 +1,133 @@ +{ + /* Single-line C-style comment */ + "max_open_trades": 3, + /* + * Multi-line C-style comment + */ + "stake_currency": "BTC", + "stake_amount": 0.05, + "fiat_display_currency": "USD", // C++-style comment + "amount_reserve_percent" : 0.05, // And more, tabs before this comment + "dry_run": false, + "ticker_interval": "5m", + "trailing_stop": false, + "trailing_stop_positive": 0.005, + "trailing_stop_positive_offset": 0.0051, + "trailing_only_offset_is_reached": false, + "minimal_roi": { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + }, + "stoploss": -0.10, + "unfilledtimeout": { + "buy": 10, + "sell": 30, // Trailing comma should also be accepted now + }, + "bid_strategy": { + "use_order_book": false, + "ask_last_balance": 0.0, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy":{ + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 9 + }, + "order_types": { + "buy": "limit", + "sell": "limit", + "stoploss": "market", + "stoploss_on_exchange": false, + "stoploss_on_exchange_interval": 60 + }, + "order_time_in_force": { + "buy": "gtc", + "sell": "gtc" + }, + "pairlist": { + "method": "VolumePairList", + "config": { + "number_assets": 20, + "sort_key": "quoteVolume", + "precision_filter": false + } + }, + "exchange": { + "name": "bittrex", + "sandbox": false, + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "password": "", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": false, + "rateLimit": 500, + "aiohttp_trust_env": false + }, + "pair_whitelist": [ + "ETH/BTC", + "LTC/BTC", + "ETC/BTC", + "DASH/BTC", + "ZEC/BTC", + "XLM/BTC", + "NXT/BTC", + "POWR/BTC", + "ADA/BTC", + "XMR/BTC" + ], + "pair_blacklist": [ + "DOGE/BTC" + ], + "outdated_offset": 5, + "markets_refresh_interval": 60 + }, + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "capital_available_percentage": 0.5, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "experimental": { + "use_sell_signal": false, + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false + }, + "telegram": { +// We can now comment out some settings +// "enabled": true, + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "db_url": "sqlite:///tradesv3.sqlite", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + }, + "strategy": "DefaultStrategy", + "strategy_path": "user_data/strategies/" +} diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index e325a0de2..0a8381089 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -634,6 +634,17 @@ def test_validate_tsl(default_conf): configuration._validate_config_consistency(default_conf) +def test_load_config_test_comments() -> None: + """ + Load config with comments + """ + config_file = Path(__file__).parents[0] / "config_test_comments.json" + print(config_file) + conf = load_config_file(str(config_file)) + + assert conf + + def test_load_config_default_exchange(all_conf) -> None: """ config['exchange'] subtree has required options in it From e5dcd520bae7516c37eba0cd3c12484e1a0210f4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 12 Aug 2019 02:19:50 +0300 Subject: [PATCH 128/269] cosmetics in sample_hyperopt and default_hyperopt --- freqtrade/optimize/default_hyperopt.py | 41 ++++++++++++++++-------- user_data/hyperopts/sample_hyperopt.py | 44 ++++++++++++++++---------- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index e05dfc95c..2554982ad 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -14,36 +14,48 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt class DefaultHyperOpts(IHyperOpt): """ Default hyperopt provided by the Freqtrade bot. - You can override it with your own hyperopt + You can override it with your own Hyperopt """ @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Add several indicators needed for buy and sell strategies defined below. + """ + # ADX dataframe['adx'] = ta.ADX(dataframe) + # MACD macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] + # MFI dataframe['mfi'] = ta.MFI(dataframe) + # RSI dataframe['rsi'] = ta.RSI(dataframe) + # Stochastic Fast stoch_fast = ta.STOCHF(dataframe) dataframe['fastd'] = stoch_fast['fastd'] + # Minus-DI dataframe['minus_di'] = ta.MINUS_DI(dataframe) # Bollinger bands bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) dataframe['bb_lowerband'] = bollinger['lower'] dataframe['bb_upperband'] = bollinger['upper'] + # SAR dataframe['sar'] = ta.SAR(dataframe) + return dataframe @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Define the buy strategy parameters to be used by hyperopt + 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 + Buy strategy Hyperopt will build and use. """ conditions = [] + # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: conditions.append(dataframe['mfi'] < params['mfi-value']) @@ -79,7 +91,7 @@ class DefaultHyperOpts(IHyperOpt): @staticmethod def indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching strategy parameters + Define your Hyperopt space for searching buy strategy parameters. """ return [ Integer(10, 25, name='mfi-value'), @@ -96,14 +108,14 @@ class DefaultHyperOpts(IHyperOpt): @staticmethod def sell_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Define the sell strategy parameters to be used by hyperopt + 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 + Sell strategy Hyperopt will build and use. """ - # print(params) conditions = [] + # GUARDS AND TRENDS if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: conditions.append(dataframe['mfi'] > params['sell-mfi-value']) @@ -139,7 +151,7 @@ class DefaultHyperOpts(IHyperOpt): @staticmethod def sell_indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching sell strategy parameters + Define your Hyperopt space for searching sell strategy parameters. """ return [ Integer(75, 100, name='sell-mfi-value'), @@ -157,9 +169,9 @@ class DefaultHyperOpts(IHyperOpt): def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Based on TA indicators. Should be a copy of from strategy - must align to populate_indicators in this file - Only used when --spaces does not include buy + Based on TA indicators. Should be a copy of same method from strategy. + Must align to populate_indicators in this file. + Only used when --spaces does not include buy space. """ dataframe.loc[ ( @@ -174,9 +186,9 @@ class DefaultHyperOpts(IHyperOpt): def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Based on TA indicators. Should be a copy of from strategy - must align to populate_indicators in this file - Only used when --spaces does not include sell + Based on TA indicators. Should be a copy of same method from strategy. + Must align to populate_indicators in this file. + Only used when --spaces does not include sell space. """ dataframe.loc[ ( @@ -186,4 +198,5 @@ class DefaultHyperOpts(IHyperOpt): (dataframe['fastd'] > 54) ), 'sell'] = 1 + return dataframe diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 1a3823afa..fabfdb23e 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -1,11 +1,10 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement 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 numpy as np import talib.abstract as ta from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real @@ -16,7 +15,7 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt class SampleHyperOpts(IHyperOpt): """ - This is a sample hyperopt to inspire you. + This is a sample Hyperopt to inspire you. Feel free to customize it. More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md @@ -37,32 +36,44 @@ class SampleHyperOpts(IHyperOpt): """ @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Add several indicators needed for buy and sell strategies defined below. + """ + # ADX dataframe['adx'] = ta.ADX(dataframe) + # MACD macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] + # MFI dataframe['mfi'] = ta.MFI(dataframe) + # RSI dataframe['rsi'] = ta.RSI(dataframe) + # Stochastic Fast stoch_fast = ta.STOCHF(dataframe) dataframe['fastd'] = stoch_fast['fastd'] + # Minus-DI dataframe['minus_di'] = ta.MINUS_DI(dataframe) # Bollinger bands bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) dataframe['bb_lowerband'] = bollinger['lower'] dataframe['bb_upperband'] = bollinger['upper'] + # SAR dataframe['sar'] = ta.SAR(dataframe) + return dataframe @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Define the buy strategy parameters to be used by hyperopt + 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 + Buy strategy Hyperopt will build and use. """ conditions = [] + # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: conditions.append(dataframe['mfi'] < params['mfi-value']) @@ -98,7 +109,7 @@ class SampleHyperOpts(IHyperOpt): @staticmethod def indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching strategy parameters + Define your Hyperopt space for searching buy strategy parameters. """ return [ Integer(10, 25, name='mfi-value'), @@ -115,14 +126,14 @@ class SampleHyperOpts(IHyperOpt): @staticmethod def sell_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Define the sell strategy parameters to be used by hyperopt + 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 + Sell strategy Hyperopt will build and use. """ - # print(params) conditions = [] + # GUARDS AND TRENDS if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: conditions.append(dataframe['mfi'] > params['sell-mfi-value']) @@ -158,7 +169,7 @@ class SampleHyperOpts(IHyperOpt): @staticmethod def sell_indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching sell strategy parameters + Define your Hyperopt space for searching sell strategy parameters. """ return [ Integer(75, 100, name='sell-mfi-value'), @@ -176,9 +187,9 @@ class SampleHyperOpts(IHyperOpt): def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Based on TA indicators. Should be a copy of from strategy - must align to populate_indicators in this file - Only used when --spaces does not include buy + Based on TA indicators. Should be a copy of same method from strategy. + Must align to populate_indicators in this file. + Only used when --spaces does not include buy space. """ dataframe.loc[ ( @@ -193,9 +204,9 @@ class SampleHyperOpts(IHyperOpt): def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Based on TA indicators. Should be a copy of from strategy - must align to populate_indicators in this file - Only used when --spaces does not include sell + Based on TA indicators. Should be a copy of same method from strategy. + Must align to populate_indicators in this file. + Only used when --spaces does not include sell space. """ dataframe.loc[ ( @@ -205,4 +216,5 @@ class SampleHyperOpts(IHyperOpt): (dataframe['fastd'] > 54) ), 'sell'] = 1 + return dataframe From 197ce0b67002161557ea4e8bd0ea62f44655f32c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 06:35:47 +0200 Subject: [PATCH 129/269] Improve documentation wording for multiconfig files --- docs/bot-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4e3f55d91..988e08029 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -57,7 +57,7 @@ freqtrade -c path/far/far/away/config.json The bot allows you to use multiple configuration files by specifying multiple `-c/--config` configuration options in the command line. Configuration parameters defined in latter configuration files override parameters with the same name -defined in the earlier configuration files specified in the command line. +defined in the previous configuration files specified in the command line earlier. For example, you can make a separate configuration file with your key and secrete for the Exchange you use for trading, specify default configuration file with From 43b41324e2517924ae24e9eca35d0568bcf4bd87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 06:45:27 +0200 Subject: [PATCH 130/269] Improve hyperopt-loss docs --- docs/bot-usage.md | 9 +++++---- docs/hyperopt.md | 6 +++++- freqtrade/configuration/cli_options.py | 4 +++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 0ca2f3cc5..9833bb43d 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -256,12 +256,13 @@ optional arguments: --continue Continue hyperopt from previous runs. By default, temporary files will be removed and hyperopt will start from scratch. - --hyperopt-loss NAME - Specify the class name of the hyperopt loss function + --hyperopt-loss NAME Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the - target for optimization is different. (default: - `DefaultHyperOptLoss`). + target for optimization is different. Built-in + Hyperopt-loss-functions are: DefaultHyperOptLoss, + OnlyProfitHyperOptLoss, SharpeHyperOptLoss. + (default: `DefaultHyperOptLoss`). ``` ## Edge commands diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6ef68d82f..918361c52 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -164,7 +164,11 @@ By default, FreqTrade uses a loss function, which has been with freqtrade since A different loss function can be specified by using the `--hyperopt-loss ` argument. This class should be in its own file within the `user_data/hyperopts/` directory. -Currently, the following loss functions are builtin: `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function), `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns) and `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration). +Currently, the following loss functions are builtin: + +* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) +* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) +* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns) ### Creating and using a custom loss function diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 04554c386..771cdfb80 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -226,7 +226,9 @@ AVAILABLE_CLI_OPTIONS = { '--hyperopt-loss', help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' - 'since the target for optimization is different. (default: `%(default)s`).', + 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' + 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.' + '(default: `%(default)s`).', metavar='NAME', default=constants.DEFAULT_HYPEROPT_LOSS, ), From af67bbde311b249910da29ed09fca1f5ec688639 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 15:43:10 +0200 Subject: [PATCH 131/269] Test timeframe_to_x --- freqtrade/tests/exchange/test_exchange.py | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e8a7201f1..245814a36 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,7 +14,9 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import API_RETRY_COUNT +from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, + timeframe_to_msecs, + timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1540,3 +1542,24 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."): ex.get_valid_pair_combination("NOPAIR", "ETH") + + +def test_timeframe_to_minutes(): + assert timeframe_to_minutes("5m") == 5 + assert timeframe_to_minutes("10m") == 10 + assert timeframe_to_minutes("1h") == 60 + assert timeframe_to_minutes("1d") == 1440 + + +def test_timeframe_to_seconds(): + assert timeframe_to_seconds("5m") == 300 + assert timeframe_to_seconds("10m") == 600 + assert timeframe_to_seconds("1h") == 3600 + assert timeframe_to_seconds("1d") == 86400 + + +def test_timeframe_to_msecs(): + assert timeframe_to_msecs("5m") == 300000 + assert timeframe_to_msecs("10m") == 600000 + assert timeframe_to_msecs("1h") == 3600000 + assert timeframe_to_msecs("1d") == 86400000 From 933a553dd436eeadf1b7a65b2120a5fea0211779 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:07:19 +0200 Subject: [PATCH 132/269] Convert timeframe to next date --- freqtrade/exchange/exchange.py | 18 ++++++++++++--- freqtrade/tests/exchange/test_exchange.py | 28 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 657f382d8..3d2bf07c7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -6,7 +6,7 @@ import asyncio import inspect import logging from copy import deepcopy -from datetime import datetime +from datetime import datetime, timezone from math import ceil, floor from random import randint from typing import Any, Dict, List, Optional, Tuple @@ -781,13 +781,25 @@ def timeframe_to_seconds(ticker_interval: str) -> int: def timeframe_to_minutes(ticker_interval: str) -> int: """ - Same as above, but returns minutes. + Same as timeframe_to_seconds, but returns minutes. """ return ccxt.Exchange.parse_timeframe(ticker_interval) // 60 def timeframe_to_msecs(ticker_interval: str) -> int: """ - Same as above, but returns milliseconds. + Same as timeframe_to_seconds, but returns milliseconds. """ return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 + + +def timeframe_to_next_date(timeframe: str, date: datetime = None): + """ + Use Timeframe and determine next candle. + """ + if not date: + date = datetime.utcnow() + timeframe_secs = timeframe_to_seconds(timeframe) + offset = date.timestamp() % timeframe_secs + new_timestamp = date.timestamp() + (timeframe_secs - offset) + return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 245814a36..23384aebe 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,7 +14,9 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, +from freqtrade.exchange.exchange import (API_RETRY_COUNT, + timeframe_to_next_date, + timeframe_to_minutes, timeframe_to_msecs, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver @@ -1563,3 +1565,27 @@ def test_timeframe_to_msecs(): assert timeframe_to_msecs("10m") == 600000 assert timeframe_to_msecs("1h") == 3600000 assert timeframe_to_msecs("1d") == 86400000 + + +def test_timeframe_to_next_date(): + # 2019-08-12 13:22:08 + date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + + # 5m -> 2019-08-12 13:25:00 + assert timeframe_to_next_date("5m", date) == datetime( + 2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc) + # 10m -> 2019-08-12 13:30:00 + assert timeframe_to_next_date("10m", date) == datetime( + 2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) + # 1h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("1h", date) == datetime( + 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) + # 2h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("2h", date) == datetime( + 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) + # 4h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("4h", date) == datetime( + 2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc) + # 1d -> 2019-08-13 00:00:00 + assert timeframe_to_next_date("1d", date) == datetime( + 2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc) From dd0ba183f88538f51aabc5db677d374a5ee12401 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:11:43 +0200 Subject: [PATCH 133/269] Add timeframe_to_prev_candle --- freqtrade/exchange/exchange.py | 16 ++++++++++++ freqtrade/tests/exchange/test_exchange.py | 30 ++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3d2bf07c7..abb03b86d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -793,6 +793,20 @@ def timeframe_to_msecs(ticker_interval: str) -> int: return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 +def timeframe_to_prev_date(timeframe: str, date: datetime = None): + """ + Use Timeframe and determine last possible candle. + """ + if not date: + date = datetime.utcnow() + timeframe_secs = timeframe_to_seconds(timeframe) + # Get offset based on timerame_secs + offset = date.timestamp() % timeframe_secs + # Subtract seconds passed since last offset + new_timestamp = date.timestamp() - offset + return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) + + def timeframe_to_next_date(timeframe: str, date: datetime = None): """ Use Timeframe and determine next candle. @@ -800,6 +814,8 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None): if not date: date = datetime.utcnow() timeframe_secs = timeframe_to_seconds(timeframe) + # Get offset to prev timeframe offset = date.timestamp() % timeframe_secs + # Add remaining seconds to next timeframe new_timestamp = date.timestamp() + (timeframe_secs - offset) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 23384aebe..e342af604 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,10 +14,10 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import (API_RETRY_COUNT, - timeframe_to_next_date, - timeframe_to_minutes, +from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, + timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1567,6 +1567,30 @@ def test_timeframe_to_msecs(): assert timeframe_to_msecs("1d") == 86400000 +def test_timeframe_to_prev_date(): + # 2019-08-12 13:22:08 + date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + + # 5m -> 2019-08-12 13:20:00 + assert timeframe_to_prev_date("5m", date) == datetime( + 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + # 10m -> 2019-08-12 13:20:00 + assert timeframe_to_prev_date("10m", date) == datetime( + 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + # 1h -> 2019-08-12 13:00:00 + assert timeframe_to_prev_date("1h", date) == datetime( + 2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc) + # 2h -> 2019-08-12 12:00:00 + assert timeframe_to_prev_date("2h", date) == datetime( + 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) + # 4h -> 2019-08-12 12:00:00 + assert timeframe_to_prev_date("4h", date) == datetime( + 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) + # 1d -> 2019-08-12 00:00:00 + assert timeframe_to_prev_date("1d", date) == datetime( + 2019, 8, 12, 0, 0, 0, tzinfo=timezone.utc) + + def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) From 1ce63b5b423e8dfa55be28ff9c614b84a6a8cef9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:17:06 +0200 Subject: [PATCH 134/269] Reformat tests to be easier readable --- freqtrade/exchange/exchange.py | 10 +++- freqtrade/tests/exchange/test_exchange.py | 68 +++++++++++------------ 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index abb03b86d..fe5561be2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -793,9 +793,12 @@ def timeframe_to_msecs(ticker_interval: str) -> int: return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 -def timeframe_to_prev_date(timeframe: str, date: datetime = None): +def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: """ Use Timeframe and determine last possible candle. + :param timeframe: timeframe in string format (e.g. "5m") + :param date: date to use. Defaults to utcnow() + :returns: date of previous candle (with utc timezone) """ if not date: date = datetime.utcnow() @@ -807,9 +810,12 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None): return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) -def timeframe_to_next_date(timeframe: str, date: datetime = None): +def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: """ Use Timeframe and determine next candle. + :param timeframe: timeframe in string format (e.g. "5m") + :param date: date to use. Defaults to utcnow() + :returns: date of next candle (with utc timezone) """ if not date: date = datetime.utcnow() diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e342af604..23604f44f 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1571,45 +1571,41 @@ def test_timeframe_to_prev_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) - # 5m -> 2019-08-12 13:20:00 - assert timeframe_to_prev_date("5m", date) == datetime( - 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) - # 10m -> 2019-08-12 13:20:00 - assert timeframe_to_prev_date("10m", date) == datetime( - 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) - # 1h -> 2019-08-12 13:00:00 - assert timeframe_to_prev_date("1h", date) == datetime( - 2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc) - # 2h -> 2019-08-12 12:00:00 - assert timeframe_to_prev_date("2h", date) == datetime( - 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) - # 4h -> 2019-08-12 12:00:00 - assert timeframe_to_prev_date("4h", date) == datetime( - 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) - # 1d -> 2019-08-12 00:00:00 - assert timeframe_to_prev_date("1d", date) == datetime( - 2019, 8, 12, 0, 0, 0, tzinfo=timezone.utc) + tf_list = [ + # 5m -> 2019-08-12 13:20:00 + ("5m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)), + # 10m -> 2019-08-12 13:20:00 + ("10m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)), + # 1h -> 2019-08-12 13:00:00 + ("1h", datetime(2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc)), + # 2h -> 2019-08-12 12:00:00 + ("2h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)), + # 4h -> 2019-08-12 12:00:00 + ("4h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)), + # 1d -> 2019-08-12 00:00:00 + ("1d", datetime(2019, 8, 12, 00, 00, 0, tzinfo=timezone.utc)), + ] + for interval, result in tf_list: + assert timeframe_to_prev_date(interval, date) == result def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + tf_list = [ + # 5m -> 2019-08-12 13:25:00 + ("5m", datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)), + # 10m -> 2019-08-12 13:30:00 + ("10m", datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)), + # 1h -> 2019-08-12 14:00:00 + ("1h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)), + # 2h -> 2019-08-12 14:00:00 + ("2h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)), + # 4h -> 2019-08-12 14:00:00 + ("4h", datetime(2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc)), + # 1d -> 2019-08-13 00:00:00 + ("1d", datetime(2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc)), + ] - # 5m -> 2019-08-12 13:25:00 - assert timeframe_to_next_date("5m", date) == datetime( - 2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc) - # 10m -> 2019-08-12 13:30:00 - assert timeframe_to_next_date("10m", date) == datetime( - 2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) - # 1h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("1h", date) == datetime( - 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) - # 2h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("2h", date) == datetime( - 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) - # 4h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("4h", date) == datetime( - 2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc) - # 1d -> 2019-08-13 00:00:00 - assert timeframe_to_next_date("1d", date) == datetime( - 2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc) + for interval, result in tf_list: + assert timeframe_to_next_date(interval, date) == result From c042d08bb73bbb00be88fb1afed5b2f147972035 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:29:09 +0200 Subject: [PATCH 135/269] Add lock_pairs to interface --- freqtrade/strategy/interface.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 37aa97bb1..363b0739a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -107,6 +107,7 @@ class IStrategy(ABC): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} + self._pair_locked_until: Dict[str, datetime] = {} @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -154,6 +155,13 @@ class IStrategy(ABC): """ return self.__class__.__name__ + def lock_pair(self, pair: str, until: datetime) -> None: + """ + Locks pair until a given timestamp happens. + Locked pairs are not analyzed, and are prevented from opening new trades. + """ + self._pair_locked_until['pair'] = until + def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Parses the given ticker history and returns a populated DataFrame From 241d5100965ab0180a3393e06f37354c040ab2cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:34:55 +0200 Subject: [PATCH 136/269] Handle and update sell-orders immediately if they are closed --- freqtrade/freqtradebot.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 603b0631f..b384902b2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -875,15 +875,18 @@ class FreqtradeBot(object): logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") # Execute sell and update trade record - order_id = self.exchange.sell(pair=str(trade.pair), - ordertype=self.strategy.order_types[sell_type], - amount=trade.amount, rate=limit, - time_in_force=self.strategy.order_time_in_force['sell'] - )['id'] + order = self.exchange.sell(pair=str(trade.pair), + ordertype=self.strategy.order_types[sell_type], + amount=trade.amount, rate=limit, + time_in_force=self.strategy.order_time_in_force['sell'] + ) - trade.open_order_id = order_id + trade.open_order_id = order['id'] trade.close_rate_requested = limit trade.sell_reason = sell_reason.value + # In case of market sell orders the order can be closed immediately + if order.get('status', 'unknown') == 'closed': + trade.update(order) Trade.session.flush() self._notify_sell(trade) From bb0b1600016b87796d4949c1dae5ecd4d10c7e18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:39:21 +0200 Subject: [PATCH 137/269] Remove duplicate test --- freqtrade/tests/test_freqtradebot.py | 47 ---------------------------- 1 file changed, 47 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4e649250a..8c241197b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2374,53 +2374,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 -def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, - ticker_sell_up, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Increase the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up - ) - freqtrade.config = {} - - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'profit', - 'limit': 1.172e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.172e-05, - 'profit_amount': 6.126e-05, - 'profit_percent': 0.0611052, - 'sell_reason': SellType.ROI.value - - } == last_msg - - def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 444ee274d727cfe280d03f115b56226556a70d64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:46:34 +0200 Subject: [PATCH 138/269] close dry-run orders in case of market orders --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 657f382d8..5bde4ce98 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -371,7 +371,7 @@ class Exchange(object): 'side': side, 'remaining': amount, 'datetime': arrow.utcnow().isoformat(), - 'status': "open", + 'status': "closed" if ordertype == "market" else "open", 'fee': None, "info": {} } From feced71a6d8fff0bf4fdd5d1a5db45b948d3182c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:47:00 +0200 Subject: [PATCH 139/269] Test closing sell-orders immediately --- freqtrade/tests/test_freqtradebot.py | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 8c241197b..9ced5c5a1 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2374,6 +2374,58 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 +def test_execute_sell_market_order(default_conf, ticker, fee, + ticker_sell_up, markets, mocker) -> None: + rpc_mock = patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # Increase the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker_sell_up + ) + freqtrade.config['order_types']['sell'] = 'market' + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + + assert not trade.is_open + assert trade.close_profit == 0.0611052 + + assert rpc_mock.call_count == 2 + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'limit': 1.172e-05, + 'amount': 90.99181073703367, + 'order_type': 'market', + 'open_rate': 1.099e-05, + 'current_rate': 1.172e-05, + 'profit_amount': 6.126e-05, + 'profit_percent': 0.0611052, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + 'sell_reason': SellType.ROI.value + + } == last_msg + + def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 0bd71db5df0c66762bd32b01ef9f3a4d3017140d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2019 15:25:05 +0000 Subject: [PATCH 140/269] Update scipy from 1.3.0 to 1.3.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6420c7879..9d558b5b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ numpy==1.17.0 pandas==0.25.0 -scipy==1.3.0 +scipy==1.3.1 From c4cdd85e807aa763fbf9271d3035fa40e4a40e62 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2019 15:25:06 +0000 Subject: [PATCH 141/269] Update ccxt from 1.18.1021 to 1.18.1043 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 1413539c3..8eabfc5ea 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.18.1021 +ccxt==1.18.1043 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.4 From 6f42d6658fcc99b6f5f98ed6aee2778118c5ce0e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2019 15:25:08 +0000 Subject: [PATCH 142/269] Update arrow from 0.14.4 to 0.14.5 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 8eabfc5ea..52b5cc590 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.1043 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 -arrow==0.14.4 +arrow==0.14.5 cachetools==3.1.1 requests==2.22.0 urllib3==1.25.3 From dd30d746884fd805fc89520c90dba79c61cdd03c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2019 15:25:09 +0000 Subject: [PATCH 143/269] Update python-rapidjson from 0.7.2 to 0.8.0 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 52b5cc590..19beec64b 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -23,7 +23,7 @@ filelock==3.0.12 py_find_1st==1.1.4 #Load ticker files 30% faster -python-rapidjson==0.7.2 +python-rapidjson==0.8.0 # Notify systemd sdnotify==0.3.2 From 200b6ea10faccbe0ce8f57aa221bf8301cfd8eda Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 19:50:22 +0200 Subject: [PATCH 144/269] Add is_pair_locked --- freqtrade/strategy/interface.py | 19 +++++++++++++++---- freqtrade/tests/strategy/test_interface.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 363b0739a..e887da43b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -4,7 +4,7 @@ This module defines the interface to apply for strategies """ import logging from abc import ABC, abstractmethod -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import Dict, List, NamedTuple, Optional, Tuple import warnings @@ -159,8 +159,19 @@ class IStrategy(ABC): """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. + :param pair: Pair to lock + :param until: datetime in UTC until the pair should be blocked from opening new trades. + Needs to be timezone aware `datetime.now(timezone.utc)` """ - self._pair_locked_until['pair'] = until + self._pair_locked_until[pair] = until + + def is_pair_locked(self, pair: str) -> bool: + """ + Checks if a pair is currently locked + """ + if pair not in self._pair_locked_until: + return False + return self._pair_locked_until[pair] >= datetime.now(timezone.utc) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -268,8 +279,8 @@ class IStrategy(ABC): sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ - This function evaluate if on the condition required to trigger a sell has been reached - if the threshold is reached and updates the trade record. + This function evaluate if one of the conditions required to trigger a sell + has been reached, which can either be a stop-loss, ROI or sell-signal. :param low: Only used during backtesting to simulate stoploss :param high: Only used during backtesting, to simulate ROI :param force_stoploss: Externally provided stoploss diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 0eb7630a1..36c9ffcd4 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -286,3 +286,19 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) - assert ret['sell'].sum() == 0 assert not log_has('TA Analysis Launched', caplog) assert log_has('Skipping TA Analysis for already analyzed candle', caplog) + + +def test_is_pair_locked(default_conf): + strategy = DefaultStrategy(default_conf) + # dict should be empty + assert not strategy._pair_locked_until + + pair = 'ETH/BTC' + assert not strategy.is_pair_locked(pair) + strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert strategy.is_pair_locked(pair) + + # XRP/BTC should not be locked now + pair = 'XRP/BTC' + assert not strategy.is_pair_locked(pair) From 2600cb7b641dd6ce0ab5da9aafb5ba3056ad8274 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:04:19 +0200 Subject: [PATCH 145/269] simplify timeframe_next_date calculation --- freqtrade/exchange/__init__.py | 4 +++- freqtrade/exchange/exchange.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 5c58320f6..828042911 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -5,6 +5,8 @@ from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 available_exchanges) from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_minutes, - timeframe_to_msecs) + timeframe_to_msecs, + timeframe_to_next_date, + timeframe_to_prev_date) from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401 diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fe5561be2..d0432a060 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -817,11 +817,9 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: :param date: date to use. Defaults to utcnow() :returns: date of next candle (with utc timezone) """ - if not date: - date = datetime.utcnow() + prevdate = timeframe_to_prev_date(timeframe, date) timeframe_secs = timeframe_to_seconds(timeframe) - # Get offset to prev timeframe - offset = date.timestamp() % timeframe_secs - # Add remaining seconds to next timeframe - new_timestamp = date.timestamp() + (timeframe_secs - offset) + + # Add one interval to previous candle + new_timestamp = prevdate.timestamp() + timeframe_secs return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) From 8f9291285238037d5483807e479741cde9f3de33 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 21:59:44 +0300 Subject: [PATCH 146/269] final colorization schema colorization schema-2: red, green, bright/dim colorization schema-3: red, green, bright only green bests colorization schema-4: no red, green for profit, bright for bests --- freqtrade/optimize/hyperopt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 802c848f1..550f13e28 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -163,11 +163,9 @@ class Hyperopt(Backtesting): # Colorize output if self.config.get('print_colorized', False): if results['total_profit'] > 0: - log_str = Style.BRIGHT + log_str - if results['loss'] >= MAX_LOSS: - log_str = Fore.RED + log_str - elif is_best_loss: log_str = Fore.GREEN + log_str + if print_all and is_best_loss: + log_str = Style.BRIGHT + log_str if print_all: print(log_str) else: From 1a34b9b61ca481b5a7472723c8a780ce45c4a194 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 12 Aug 2019 21:07:29 +0300 Subject: [PATCH 147/269] --no-color option introduced --- freqtrade/configuration/cli_options.py | 9 +++++---- freqtrade/configuration/configuration.py | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index aab3c12b3..a5784b0c1 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -192,10 +192,11 @@ AVAILABLE_CLI_OPTIONS = { default=False, ), "print_colorized": Arg( - '--color', '--print-colorized', - help='Print colorized hyperopt results.', - action='store_true', - default=False + '--no-color', + help='Disable colorization of hyperopt results. May be useful if you are ' + 'redirecting output to a file.', + action='store_false', + default=True, ), "hyperopt_jobs": Arg( '-j', '--job-workers', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 01732ca9e..4d85d5c28 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -254,8 +254,9 @@ class Configuration(object): self._args_to_config(config, argname='print_all', logstring='Parameter --print-all detected ...') - self._args_to_config(config, argname='print_colorized', - logstring='Parameter --color/--print-colorized detected ...') + if 'print_colorized' in self.args and not self.args.print_colorized: + logger.info('Parameter --no-color detected ...') + config.update({'print_colorized': getattr(self.args, 'print_colorized')}) self._args_to_config(config, argname='hyperopt_jobs', logstring='Parameter -j/--job-workers detected: {}') From 23a70932d299b00f6d3f44e5c95676adfca8b71c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:36:45 +0200 Subject: [PATCH 148/269] Remove pointless tests (without config?? really?) --- freqtrade/tests/test_freqtradebot.py | 94 ---------------------------- 1 file changed, 94 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4e649250a..41ce661b9 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2374,100 +2374,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 -def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, - ticker_sell_up, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Increase the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up - ) - freqtrade.config = {} - - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'profit', - 'limit': 1.172e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.172e-05, - 'profit_amount': 6.126e-05, - 'profit_percent': 0.0611052, - 'sell_reason': SellType.ROI.value - - } == last_msg - - -def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, - ticker_sell_down, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Decrease the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down - ) - - freqtrade.config = {} - freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'loss', - 'limit': 1.044e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.044e-05, - 'profit_amount': -5.492e-05, - 'profit_percent': -0.05478342, - 'sell_reason': SellType.STOP_LOSS.value - } == last_msg - - def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) From ca739f71fb17b0c0437745ab257f67afc3b9823a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:37:11 +0200 Subject: [PATCH 149/269] Fix default argument handling for timeframe_to_nextdate --- freqtrade/exchange/exchange.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d0432a060..e36032c49 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -801,7 +801,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: :returns: date of previous candle (with utc timezone) """ if not date: - date = datetime.utcnow() + date = datetime.now(timezone.utc) timeframe_secs = timeframe_to_seconds(timeframe) # Get offset based on timerame_secs offset = date.timestamp() % timeframe_secs diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 23604f44f..3747a2ad3 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1588,6 +1588,9 @@ def test_timeframe_to_prev_date(): for interval, result in tf_list: assert timeframe_to_prev_date(interval, date) == result + date = datetime.now(tz=timezone.utc) + assert timeframe_to_prev_date("5m", date) < date + def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 @@ -1609,3 +1612,6 @@ def test_timeframe_to_next_date(): for interval, result in tf_list: assert timeframe_to_next_date(interval, date) == result + + date = datetime.now(tz=timezone.utc) + assert timeframe_to_next_date("5m", date) > date From 59acd5ec7c0b5927f1386ab6a10ee948b9ad9612 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:39:34 +0200 Subject: [PATCH 150/269] Lock pair for the rest of the candle in case of sells --- freqtrade/freqtradebot.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 603b0631f..c14392752 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver @@ -284,6 +284,9 @@ class FreqtradeBot(object): # running get_signal on historical data fetched for _pair in whitelist: + if self.strategy.is_pair_locked(_pair): + logger.info(f"Pair {_pair} is currently locked.") + continue (buy, sell) = self.strategy.get_signal( _pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) @@ -885,6 +888,10 @@ class FreqtradeBot(object): trade.close_rate_requested = limit trade.sell_reason = sell_reason.value Trade.session.flush() + + # Lock pair for one candle to prevent immediate rebuys + self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval'])) + self._notify_sell(trade) def _notify_sell(self, trade: Trade): From 58d308fd05cef7339445fb91226ff6f052021ca0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 12 Aug 2019 23:13:04 +0300 Subject: [PATCH 151/269] fix handling --no-color for edge and backtesting --- freqtrade/configuration/configuration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4d85d5c28..a2e16d5d9 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -256,7 +256,9 @@ class Configuration(object): if 'print_colorized' in self.args and not self.args.print_colorized: logger.info('Parameter --no-color detected ...') - config.update({'print_colorized': getattr(self.args, 'print_colorized')}) + config.update({'print_colorized': False}) + else: + config.update({'print_colorized': True}) self._args_to_config(config, argname='hyperopt_jobs', logstring='Parameter -j/--job-workers detected: {}') From 482847a99417a9c582492d685e5a225ad326150d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 13 Aug 2019 00:10:33 +0300 Subject: [PATCH 152/269] docs adjusted; various fixes to bot-usage.md and configuration.md --- docs/bot-usage.md | 15 +++++++++------ docs/configuration.md | 19 ++++++++++++++----- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 0ca2f3cc5..f720bf554 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -2,7 +2,7 @@ This page explains the different parameters of the bot and how to run it. -!Note: +!!! Note: If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. @@ -43,19 +43,22 @@ optional arguments: --sd-notify Notify systemd service manager. ``` -### How to use a different configuration file? +### How to specify which configuration file be used? -The bot allows you to select which configuration file you want to use. Per -default, the bot will load the file `./config.json` +The bot allows you to select which configuration file you want to use by means of +the `-c/--config` command line option: ```bash freqtrade -c path/far/far/away/config.json ``` +Per default, the bot loads the `config.json` configuration file from the current +working directory. + ### How to use multiple configuration files? The bot allows you to use multiple configuration files by specifying multiple -`-c/--config` configuration options in the command line. Configuration parameters +`-c/--config` options in the command line. Configuration parameters defined in the last configuration file override parameters with the same name defined in the previous configuration file specified in the command line. @@ -266,7 +269,7 @@ optional arguments: ## Edge commands -To know your trade expectacny and winrate against historical data, you can use Edge. +To know your trade expectancy and winrate against historical data, you can use Edge. ``` usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] diff --git a/docs/configuration.md b/docs/configuration.md index f8dbbbbbb..b48e23eee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,15 +1,24 @@ # Configure the bot -This page explains how to configure your `config.json` file. +This page explains how to configure your configuration file. -## Setup config.json +Per default, the bot loads configuration from the `config.json` file located in the current working directory. +You can change the configuration file used by the bot with the `-c/--config` option. -We recommend to copy and use the `config.json.example` as a template +If you used the [Quick start](installation.md/#quick-start) method for installing +the bot, the installation script should have already created the default configuration file (`config.json`) for you. + +We recommend you to copy and use the `config.json.example` as a template for your bot configuration. -The table below will list all configuration parameters. +The configuration file defines the set of configuration parameters for the bot written in the JSON format. +Additionally, you may use one-line `// ...` and multi-line `/* ... */` comments. -Mandatory Parameters are marked as **Required**. +## Configuration parameters + +The table below will list all configuration parameters available. + +Mandatory parameters are marked as **Required**. | Command | Default | Description | |----------|---------|-------------| From de802341651109b31884d3af3931123d16fe590b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 13 Aug 2019 00:23:41 +0300 Subject: [PATCH 153/269] hyperopt options updated in bot-usage.md --- docs/bot-usage.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8877ed010..e3db2ab33 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -207,7 +207,7 @@ usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--customhyperopt NAME] [--hyperopt-path PATH] [--eps] [-e INT] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] - [--dmmp] [--print-all] [-j JOBS] + [--dmmp] [--print-all] [--no-color] [-j JOBS] [--random-state INT] [--min-trades INT] [--continue] [--hyperopt-loss NAME] @@ -243,6 +243,8 @@ optional arguments: (same as setting `max_open_trades` to a very high number). --print-all Print all results, not only the best ones. + --no-color Disable colorization of hyperopt results. May be + useful if you are redirecting output to a file. -j JOBS, --job-workers JOBS The number of concurrently running jobs for hyperoptimization (hyperopt worker processes). If -1 @@ -256,8 +258,7 @@ optional arguments: --continue Continue hyperopt from previous runs. By default, temporary files will be removed and hyperopt will start from scratch. - --hyperopt-loss NAME - Specify the class name of the hyperopt loss function + --hyperopt-loss NAME Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. (default: From f960ea039e472e3b29d6b3bd69842f9d7e524097 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 08:05:51 +0200 Subject: [PATCH 154/269] Remove duplicate test --- freqtrade/tests/test_freqtradebot.py | 47 ---------------------------- 1 file changed, 47 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 9ced5c5a1..a1d5f691e 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2426,53 +2426,6 @@ def test_execute_sell_market_order(default_conf, ticker, fee, } == last_msg -def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, - ticker_sell_down, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Decrease the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down - ) - - freqtrade.config = {} - freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'loss', - 'limit': 1.044e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.044e-05, - 'profit_amount': -5.492e-05, - 'profit_percent': -0.05478342, - 'sell_reason': SellType.STOP_LOSS.value - } == last_msg - - def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) From d8dbea9d5b958f14f49677565363f8dde16fa3da Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 08:20:35 +0200 Subject: [PATCH 155/269] Add exchange_reasons to bad exchanges --- freqtrade/exchange/__init__.py | 3 ++- freqtrade/exchange/exchange.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 5c58320f6..cbf851fc2 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,5 +1,6 @@ from freqtrade.exchange.exchange import Exchange # noqa: F401 -from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 +from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401 + is_exchange_bad, is_exchange_available, is_exchange_officially_supported, available_exchanges) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 657f382d8..9f363c799 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -25,6 +25,10 @@ logger = logging.getLogger(__name__) API_RETRY_COUNT = 4 +BAD_EXCHANGES = { + "bitmex": "Various reasons", + "bitstamp": "Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983", + } def retrier_async(f): @@ -755,7 +759,11 @@ class Exchange(object): def is_exchange_bad(exchange: str) -> bool: - return exchange in ['bitmex', 'bitstamp'] + return exchange in BAD_EXCHANGES + + +def get_exchange_bad_reason(exchange: str) -> str: + return BAD_EXCHANGES.get(exchange) def is_exchange_available(exchange: str, ccxt_module=None) -> bool: From 3c589bb877c3c938cd0156dd231a1a6dc274a24d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 08:26:10 +0200 Subject: [PATCH 156/269] fail if known bad exchanges are detcted --- docs/configuration.md | 1 + freqtrade/configuration/check_exchange.py | 11 +++++------ freqtrade/configuration/configuration.py | 2 +- freqtrade/exchange/exchange.py | 5 +++-- freqtrade/tests/test_configuration.py | 6 +++--- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f8dbbbbbb..fc450bd82 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -53,6 +53,7 @@ Mandatory Parameters are marked as **Required**. | `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). +| `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. | `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists). | `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index 8dae06f7a..70f4cfa33 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -2,9 +2,9 @@ import logging from typing import Any, Dict from freqtrade import OperationalException -from freqtrade.exchange import (is_exchange_bad, is_exchange_available, - is_exchange_officially_supported, available_exchanges) - +from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, + is_exchange_available, is_exchange_bad, + is_exchange_officially_supported) logger = logging.getLogger(__name__) @@ -31,9 +31,8 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: ) if check_for_bad and is_exchange_bad(exchange): - logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. ' - f'Use it only for development and testing purposes.') - return False + raise OperationalException(f'Exchange "{exchange}" is known to not work with the bot yet. ' + f'Reason: {get_exchange_bad_reason(exchange)}') if is_exchange_officially_supported(exchange): logger.info(f'Exchange "{exchange}" is officially supported ' diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 237346e37..4bbf4ddd6 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -148,7 +148,7 @@ class Configuration(object): config['internals'].update({'sd_notify': True}) # Check if the exchange set by the user is supported - check_exchange(config) + check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9f363c799..0e23cf8b8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -27,7 +27,8 @@ logger = logging.getLogger(__name__) API_RETRY_COUNT = 4 BAD_EXCHANGES = { "bitmex": "Various reasons", - "bitstamp": "Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983", + "bitstamp": "Does not provide history. " + "Details in https://github.com/freqtrade/freqtrade/issues/1983", } @@ -763,7 +764,7 @@ def is_exchange_bad(exchange: str) -> bool: def get_exchange_bad_reason(exchange: str) -> str: - return BAD_EXCHANGES.get(exchange) + return BAD_EXCHANGES.get(exchange, "") def is_exchange_available(exchange: str, ccxt_module=None) -> bool: diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 132dd334e..2bc561b7d 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -499,9 +499,9 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) - assert not check_exchange(default_conf) - assert log_has_re(r"Exchange .* is known to not work with the bot yet\. " - r"Use it only for development and testing purposes\.", caplog) + with pytest.raises(OperationalException, + match=r"Exchange .* is known to not work with the bot yet.*"): + check_exchange(default_conf) caplog.clear() # Test a 'bad' exchange with check_for_bad=False From 2961efdc1802bba064d9ead55df125fa2c0816a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:48:21 +0200 Subject: [PATCH 157/269] Initial test for locked pair --- freqtrade/tests/test_freqtradebot.py | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 41ce661b9..6f6a539eb 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2499,6 +2499,43 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke assert trade.sell_reason == SellType.SELL_SIGNAL.value +def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mocker, caplog) -> None: + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # Decrease the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker_sell_down + ) + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], + sell_reason=SellType.STOP_LOSS) + trade.close(ticker_sell_down()['bid']) + assert trade.pair in freqtrade.strategy._pair_locked_until + assert freqtrade.strategy.is_pair_locked(trade.pair) + + # reinit - should buy other pair. + caplog.clear() + freqtrade.create_trade() + + assert log_has(f"Pair {trade.pair} is currently locked.", caplog) + + def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From 28e318b6466845900d52f73e5d07c0d25d05f7ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 08:47:11 +0200 Subject: [PATCH 158/269] Lock pairs for stoploss_on_exchange fills too --- freqtrade/freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c14392752..2d1c1f566 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -681,6 +681,9 @@ class FreqtradeBot(object): if stoploss_order and stoploss_order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(stoploss_order) + # Lock pair for one candle to prevent immediate rebuys + self.strategy.lock_pair(trade.pair, + timeframe_to_next_date(self.config['ticker_interval'])) self._notify_sell(trade) return True From 8d813fa728a4d454aaa8cf95a84e507513ca5aa6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 09:36:52 +0200 Subject: [PATCH 159/269] Remove return-value for _process --- freqtrade/tests/test_freqtradebot.py | 15 +++++++-------- freqtrade/worker.py | 9 ++------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4e649250a..883e4f050 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -744,8 +744,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non worker = Worker(args=None, config=default_conf) patch_get_signal(worker.freqtrade) - result = worker._process() - assert result is False + worker._process() assert sleep_mock.has_calls() @@ -763,8 +762,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> assert worker.state == State.RUNNING - result = worker._process() - assert result is False + worker._process() assert worker.state == State.STOPPED assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] @@ -786,13 +784,14 @@ def test_process_trade_handling( trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert not trades - result = freqtrade.process() - assert result is True + freqtrade.process() + trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert len(trades) == 1 - result = freqtrade.process() - assert result is False + # Nothing happened ... + freqtrade.process() + assert len(trades) == 1 def test_process_trade_no_whitelist_pair( diff --git a/freqtrade/worker.py b/freqtrade/worker.py index db0dba0e8..df792e35e 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -127,11 +127,10 @@ class Worker(object): time.sleep(duration) return result - def _process(self) -> bool: + def _process(self) -> None: logger.debug("========================================") - state_changed = False try: - state_changed = self.freqtrade.process() + self.freqtrade.process() except TemporaryError as error: logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...") time.sleep(constants.RETRY_TIMEOUT) @@ -144,10 +143,6 @@ class Worker(object): }) logger.exception('OperationalException. Stopping trader ...') self.freqtrade.state = State.STOPPED - # TODO: The return value of _process() is not used apart tests - # and should (could) be eliminated later. See PR #1689. -# state_changed = True - return state_changed def _reconfigure(self) -> None: """ From 4b8eaaf7aa97c6bf2a0cf339ca980978f34f84a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 09:37:56 +0200 Subject: [PATCH 160/269] freqtradebot.process() does not need to return anything --- freqtrade/freqtradebot.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 603b0631f..c4792a275 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -105,13 +105,12 @@ class FreqtradeBot(object): # Adjust stoploss if it was changed Trade.stoploss_reinitialization(self.strategy.stoploss) - def process(self) -> bool: + def process(self) -> None: """ Queries the persistence layer for open trades and handles them, otherwise a new trade is created. :return: True if one or more trades has been created or closed, False otherwise """ - state_changed = False # Check whether markets have to be reloaded self.exchange._reload_markets() @@ -138,19 +137,17 @@ class FreqtradeBot(object): # First process current opened trades for trade in trades: - state_changed |= self.process_maybe_execute_sell(trade) + self.process_maybe_execute_sell(trade) # Then looking for buy opportunities if len(trades) < self.config['max_open_trades']: - state_changed = self.process_maybe_execute_buy() + self.process_maybe_execute_buy() if 'unfilledtimeout' in self.config: # Check and handle any timed out open orders self.check_handle_timedout() Trade.session.flush() - return state_changed - def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): """ Extend whitelist with pairs from open trades From c29389f5f397156819e79055e4bdf21b0b64d2d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 09:38:21 +0200 Subject: [PATCH 161/269] Remove process() checks from tests --- freqtrade/tests/test_freqtradebot.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 883e4f050..7ad9cba7a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -711,8 +711,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert not trades - result = freqtrade.process() - assert result is True + freqtrade.process() trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert len(trades) == 1 @@ -833,11 +832,10 @@ def test_process_trade_no_whitelist_pair( )) assert pair not in freqtrade.active_pair_whitelist - result = freqtrade.process() + freqtrade.process() assert pair in freqtrade.active_pair_whitelist # Make sure each pair is only in the list once assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist)) - assert result is True def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) -> None: From 8873e0072c81d27015d2e0e936f509c6c7690d83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 09:42:22 +0200 Subject: [PATCH 162/269] process_maybe_execute_buy does not need to return bool --- freqtrade/freqtradebot.py | 10 +++------- freqtrade/tests/test_freqtradebot.py | 8 +++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c4792a275..b18b8eac5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -426,21 +426,17 @@ class FreqtradeBot(object): return True - def process_maybe_execute_buy(self) -> bool: + def process_maybe_execute_buy(self) -> None: """ Tries to execute a buy trade in a safe way :return: True if executed """ try: # Create entity and execute trade - if self.create_trade(): - return True - - logger.info('Found no buy signals for whitelisted currencies. Trying again..') - return False + if not self.create_trade(): + logger.info('Found no buy signals for whitelisted currencies. Trying again...') except DependencyException as exception: logger.warning('Unable to create trade: %s', exception) - return False def process_maybe_execute_sell(self, trade: Trade) -> bool: """ diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 7ad9cba7a..f70b5374e 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1385,14 +1385,12 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, stop_price=0.00002344 * 0.99) -def test_process_maybe_execute_buy(mocker, default_conf) -> None: +def test_process_maybe_execute_buy(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=True)) - assert freqtrade.process_maybe_execute_buy() - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=False)) - assert not freqtrade.process_maybe_execute_buy() + freqtrade.process_maybe_execute_buy() + assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> None: From 997eb7574aba223f5d25e060b8bd3c488882efc6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:01:29 +0200 Subject: [PATCH 163/269] Support creating multiple trades in one iteration --- freqtrade/freqtradebot.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b18b8eac5..f5ea131e4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -279,15 +279,16 @@ class FreqtradeBot(object): logger.info("No currency pair in whitelist, but checking to sell open trades.") return False + buycount = 0 # running get_signal on historical data fetched for _pair in whitelist: (buy, sell) = self.strategy.get_signal( _pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) - if buy and not sell: + if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']: stake_amount = self._get_trade_stake_amount(_pair) if not stake_amount: - return False + continue logger.info(f"Buy signal found: about create a new trade with stake_amount: " f"{stake_amount} ...") @@ -297,12 +298,11 @@ class FreqtradeBot(object): if (bidstrat_check_depth_of_market.get('enabled', False)) and\ (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market): - return self.execute_buy(_pair, stake_amount) - else: - return False - return self.execute_buy(_pair, stake_amount) + buycount += self.execute_buy(_pair, stake_amount) - return False + buycount += self.execute_buy(_pair, stake_amount) + + return buycount > 0 def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: """ From a325f1ce2bb05df4d7e086c7b7fe0d8da21f9763 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:01:43 +0200 Subject: [PATCH 164/269] adapt some tests since create_trade() can now buy multiple times, we need to use execute_buy() to create a single trade --- freqtrade/tests/test_freqtradebot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index f70b5374e..ba978e7ad 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -253,13 +253,14 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, assert result == default_conf['stake_amount'] / conf['max_open_trades'] # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.create_trade() + # freqtrade.create_trade() + freqtrade.execute_buy('ETH/BTC', result) result = freqtrade._get_trade_stake_amount('LTC/BTC') assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1) # create 2 trades, order amount should be None - freqtrade.create_trade() + freqtrade.execute_buy('LTC/BTC', result) result = freqtrade._get_trade_stake_amount('XRP/BTC') assert result is None From 6948e0ba847bd73eafb8e47a64369c9218815706 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:12:02 +0200 Subject: [PATCH 165/269] Handle orderbook_depth check correctly --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f5ea131e4..699500628 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -299,6 +299,8 @@ class FreqtradeBot(object): (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market): buycount += self.execute_buy(_pair, stake_amount) + else: + continue buycount += self.execute_buy(_pair, stake_amount) From 974d899b337e83066ab4c52bae99d65a4f9b5543 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:12:12 +0200 Subject: [PATCH 166/269] Adapt some more tests --- freqtrade/tests/test_freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index ba978e7ad..43982fa5c 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -302,6 +302,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) + edge_conf['max_open_trades'] = float('inf') # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 # Thus, if price falls 21%, stoploss should be triggered @@ -342,6 +343,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) + edge_conf['max_open_trades'] = float('inf') # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 # Thus, if price falls 15%, stoploss should not be triggered @@ -380,6 +382,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, patch_RPCManager(mocker) patch_exchange(mocker) default_conf['stake_amount'] = 0.0000098751 + default_conf['max_open_trades'] = 2 mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, @@ -1284,7 +1287,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) - + edge_conf['max_open_trades'] = float('inf') mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock(return_value={ From d69f7ae471ccb359aa41ba80a57c60d910b1be00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:15:31 +0200 Subject: [PATCH 167/269] Adapt final tests to support multi-trade creation --- freqtrade/tests/rpc/test_rpc_telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 3575520ad..e00903947 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -308,6 +308,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, limit_sell_order, markets, mocker) -> None: patch_exchange(mocker) + default_conf['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0 @@ -357,9 +358,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, # Reset msg_mock msg_mock.reset_mock() + freqtradebot.config['max_open_trades'] = 2 # Add two other trades freqtradebot.create_trade() - freqtradebot.create_trade() trades = Trade.query.all() for trade in trades: @@ -832,14 +833,13 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker markets=PropertyMock(return_value=markets), validate_pairs=MagicMock(return_value={}) ) - + default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Create some test data - for _ in range(4): - freqtradebot.create_trade() + freqtradebot.create_trade() rpc_mock.reset_mock() update.message.text = '/forcesell all' From 0a07dfc5cf7bad17af06c1e873e363fe72dd2d46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:20:32 +0200 Subject: [PATCH 168/269] Add test verifying that multiple trades are opened in one iteration --- freqtrade/tests/test_freqtradebot.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 43982fa5c..e8245d60b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -697,6 +697,28 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: assert not freqtrade.create_trade() +@pytest.mark.parametrize("max_open", range(1, 5)) +def test_create_trade_multiple_trades(default_conf, ticker, + fee, markets, mocker, max_open) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + default_conf['max_open_trades'] = max_open + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + buy=MagicMock(return_value={'id': "12355555"}), + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + freqtrade.create_trade() + + trades = Trade.get_open_trades() + assert len(trades) == max_open + + def test_process_trade_creation(default_conf, ticker, limit_buy_order, markets, fee, mocker, caplog) -> None: patch_RPCManager(mocker) From 9d476b5ab267f4bc829f00b3de05a4b97e54e249 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:34:27 +0200 Subject: [PATCH 169/269] Also check 0 open trades --- freqtrade/tests/test_freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index e8245d60b..acab74233 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -697,7 +697,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: assert not freqtrade.create_trade() -@pytest.mark.parametrize("max_open", range(1, 5)) +@pytest.mark.parametrize("max_open", range(0, 5)) def test_create_trade_multiple_trades(default_conf, ticker, fee, markets, mocker, max_open) -> None: patch_RPCManager(mocker) From 94196c84e9f2354163d47b57009793c7ea39e730 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 13 Aug 2019 14:25:56 +0300 Subject: [PATCH 170/269] docs: explanation for --no-color and colorization schema for results --- docs/hyperopt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 16e2212d5..db6dbb343 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -343,9 +343,9 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: return dataframe ``` -You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. +By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. -When the `--color/--print-colorized` command line option is used, the results are colorized -- bad results (with zero trades or limited by the `--min-trades` option) are red, current bests -- in green, results with positive total profit are printed in bold. +You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. ### Understand Hyperopt ROI results From 4c4ba08e854bd49b56fbc243f65534fa5ddc3dad Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 13 Aug 2019 19:47:38 +0300 Subject: [PATCH 171/269] colorama added to install_requires --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 202e3fd0d..572a32ccb 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ setup(name='freqtrade', 'py_find_1st', 'python-rapidjson', 'sdnotify', + 'colorama', # from requirements.txt 'numpy', 'pandas', From 3d36747b920ed70e366b4ab0143c445078e4ce5d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 13 Aug 2019 21:52:50 +0300 Subject: [PATCH 172/269] preface in configuration.md reworked --- docs/configuration.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b48e23eee..66b4b6da2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,18 +1,28 @@ # Configure the bot -This page explains how to configure your configuration file. +This page explains how to configure the bot. + +## The Freqtrade configuration file + +The bot uses a set of configuration parameters during its operation that all together conform the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file). Per default, the bot loads configuration from the `config.json` file located in the current working directory. -You can change the configuration file used by the bot with the `-c/--config` option. + +You can change the name of the configuration file used by the bot with the `-c/--config` command line option. + +In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. If you used the [Quick start](installation.md/#quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. -We recommend you to copy and use the `config.json.example` as a template +If default configuration file is not created we recommend you to copy and use the `config.json.example` as a template for your bot configuration. -The configuration file defines the set of configuration parameters for the bot written in the JSON format. -Additionally, you may use one-line `// ...` and multi-line `/* ... */` comments. +The Freqtrade configuration file is to be written in the JSON format. + +Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters. + +Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates syntax of the configuration file at startup and will warn you if you made any errors editing it. ## Configuration parameters From e35a3492299fd4961585a38b9a3dfedcf920a05b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:07:03 +0200 Subject: [PATCH 173/269] Fix spelling of interface.py docstring --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e887da43b..99f5f26de 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -279,7 +279,7 @@ class IStrategy(ABC): sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ - This function evaluate if one of the conditions required to trigger a sell + This function evaluates if one of the conditions required to trigger a sell has been reached, which can either be a stop-loss, ROI or sell-signal. :param low: Only used during backtesting to simulate stoploss :param high: Only used during backtesting, to simulate ROI From a76136c0102cffbe3e1ca24c2b8ca88e81146558 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:16:43 +0200 Subject: [PATCH 174/269] Rename create_trade to create_trades --- freqtrade/freqtradebot.py | 4 +- freqtrade/tests/rpc/test_rpc.py | 22 ++-- freqtrade/tests/rpc/test_rpc_apiserver.py | 8 +- freqtrade/tests/rpc/test_rpc_telegram.py | 22 ++-- freqtrade/tests/test_freqtradebot.py | 127 +++++++++++----------- 5 files changed, 91 insertions(+), 92 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 699500628..e67a8687f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -256,7 +256,7 @@ class FreqtradeBot(object): amount_reserve_percent = max(amount_reserve_percent, 0.5) return min(min_stake_amounts) / amount_reserve_percent - def create_trade(self) -> bool: + def create_trades(self) -> bool: """ Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created @@ -435,7 +435,7 @@ class FreqtradeBot(object): """ try: # Create entity and execute trade - if not self.create_trade(): + if not self.create_trades(): logger.info('Found no buy signals for whitelisted currencies. Trying again...') except DependencyException as exception: logger.warning('Unable to create trade: %s', exception) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index d273244b0..5d3fb7920 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -44,7 +44,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: with pytest.raises(RPCException, match=r'.*no active trade*'): rpc._rpc_trade_status() - freqtradebot.create_trade() + freqtradebot.create_trades() results = rpc._rpc_trade_status() assert { 'trade_id': 1, @@ -116,7 +116,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: with pytest.raises(RPCException, match=r'.*no active order*'): rpc._rpc_status_table() - freqtradebot.create_trade() + freqtradebot.create_trades() result = rpc._rpc_status_table() assert 'instantly' in result['Since'].all() assert 'ETH/BTC' in result['Pair'].all() @@ -151,7 +151,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -208,7 +208,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -222,7 +222,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, trade.close_date = datetime.utcnow() trade.is_open = False - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -292,7 +292,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -536,7 +536,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: msg = rpc._rpc_forcesell('all') assert msg == {'result': 'Created sell orders for all open trades.'} - freqtradebot.create_trade() + freqtradebot.create_trades() msg = rpc._rpc_forcesell('all') assert msg == {'result': 'Created sell orders for all open trades.'} @@ -570,7 +570,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: assert cancel_order_mock.call_count == 1 assert trade.amount == filled_amount - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.filter(Trade.id == '2').first() amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it @@ -589,7 +589,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: assert cancel_order_mock.call_count == 2 assert trade.amount == amount - freqtradebot.create_trade() + freqtradebot.create_trades() # make an limit-sell open trade mocker.patch( 'freqtrade.exchange.Exchange.get_order', @@ -622,7 +622,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -660,7 +660,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: assert counts["current"] == 0 # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() counts = rpc._rpc_count() assert counts["current"] == 1 diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index a218d5622..794343a98 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -275,7 +275,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json["max"] == 1.0 # Create some test data - ftbot.create_trade() + ftbot.create_trades() rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 1.0 @@ -329,7 +329,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert len(rc.json) == 1 assert rc.json == {"error": "Error querying _profit: no closed trade"} - ftbot.create_trade() + ftbot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -418,7 +418,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert_response(rc, 502) assert rc.json == {'error': 'Error querying _status: no active trade'} - ftbot.create_trade() + ftbot.create_trades() rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 @@ -548,7 +548,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcesell: invalid argument"} - ftbot.create_trade() + ftbot.create_trades() rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index e00903947..2f469643f 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -192,7 +192,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: # Create some test data for _ in range(3): - freqtradebot.create_trade() + freqtradebot.create_trades() telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -240,7 +240,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No msg_mock.reset_mock() # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() # Trigger status while we have a fulfilled order for the open trade telegram._status(bot=MagicMock(), update=update) @@ -292,7 +292,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) msg_mock.reset_mock() # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() telegram._status_table(bot=MagicMock(), update=update) @@ -332,7 +332,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -360,7 +360,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, msg_mock.reset_mock() freqtradebot.config['max_open_trades'] = 2 # Add two other trades - freqtradebot.create_trade() + freqtradebot.create_trades() trades = Trade.query.all() for trade in trades: @@ -439,7 +439,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -734,7 +734,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -785,7 +785,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() # Decrease the price and sell it mocker.patch.multiple( @@ -839,7 +839,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() rpc_mock.reset_mock() update.message.text = '/forcesell all' @@ -983,7 +983,7 @@ def test_performance_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -1028,7 +1028,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non freqtradebot.state = State.RUNNING # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() msg_mock.reset_mock() telegram._count(bot=MagicMock(), update=update) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index acab74233..d74615d6b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -253,7 +253,6 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, assert result == default_conf['stake_amount'] / conf['max_open_trades'] # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - # freqtrade.create_trade() freqtrade.execute_buy('ETH/BTC', result) result = freqtrade._get_trade_stake_amount('LTC/BTC') @@ -327,7 +326,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) ############################################# @@ -368,7 +367,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) ############################################# @@ -392,7 +391,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade is not None @@ -400,7 +399,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, assert trade.is_open assert trade.open_date is not None - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.order_by(Trade.id.desc()).first() assert trade is not None @@ -523,7 +522,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: assert result == min(8, 2 * 2) / 0.9 -def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: +def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -538,7 +537,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocke whitelist = deepcopy(default_conf['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade is not None @@ -556,8 +555,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocke assert whitelist == default_conf['exchange']['pair_whitelist'] -def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) @@ -572,11 +571,11 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.create_trade() + freqtrade.create_trades() -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -591,13 +590,13 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] assert rate * amount >= default_conf['stake_amount'] -def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -613,11 +612,11 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert not freqtrade.create_trade() + assert not freqtrade.create_trades() -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -634,12 +633,12 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert not freqtrade.create_trade() + assert not freqtrade.create_trades() assert freqtrade._get_trade_stake_amount('ETH/BTC') is None -def test_create_trade_no_pairs_let(default_conf, ticker, limit_buy_order, fee, - markets, mocker, caplog) -> None: +def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, + markets, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -654,13 +653,13 @@ def test_create_trade_no_pairs_let(default_conf, ticker, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert freqtrade.create_trade() - assert not freqtrade.create_trade() + assert freqtrade.create_trades() + assert not freqtrade.create_trades() assert log_has("No currency pair in whitelist, but checking to sell open trades.", caplog) -def test_create_trade_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - markets, mocker, caplog) -> None: +def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, + markets, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -674,11 +673,11 @@ def test_create_trade_no_pairs_in_whitelist(default_conf, ticker, limit_buy_orde freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert not freqtrade.create_trade() + assert not freqtrade.create_trades() assert log_has("Whitelist is empty.", caplog) -def test_create_trade_no_signal(default_conf, fee, mocker) -> None: +def test_create_trades_no_signal(default_conf, fee, mocker) -> None: default_conf['dry_run'] = True patch_RPCManager(mocker) @@ -694,12 +693,12 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: Trade.query = MagicMock() Trade.query.filter = MagicMock() - assert not freqtrade.create_trade() + assert not freqtrade.create_trades() @pytest.mark.parametrize("max_open", range(0, 5)) -def test_create_trade_multiple_trades(default_conf, ticker, - fee, markets, mocker, max_open) -> None: +def test_create_trades_multiple_trades(default_conf, ticker, + fee, markets, mocker, max_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['max_open_trades'] = max_open @@ -713,7 +712,7 @@ def test_create_trade_multiple_trades(default_conf, ticker, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trades = Trade.get_open_trades() assert len(trades) == max_open @@ -1101,7 +1100,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # Fourth case: when stoploss is set and it is hit # should unset stoploss_order_id and return true # as a trade actually happened - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1176,7 +1175,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1266,7 +1265,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # setting stoploss_on_exchange_interval to 60 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1347,7 +1346,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1414,7 +1413,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, def test_process_maybe_execute_buy(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=False)) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trades', MagicMock(return_value=False)) freqtrade.process_maybe_execute_buy() assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) @@ -1423,7 +1422,7 @@ def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> No freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch( - 'freqtrade.freqtradebot.FreqtradeBot.create_trade', + 'freqtrade.freqtradebot.FreqtradeBot.create_trades', MagicMock(side_effect=DependencyException) ) freqtrade.process_maybe_execute_buy() @@ -1610,7 +1609,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -1650,7 +1649,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade, value=(True, True)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() # Buy and Sell triggering, so doing nothing ... trades = Trade.query.all() @@ -1659,7 +1658,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, # Buy is triggering, so buying ... patch_get_signal(freqtrade, value=(True, False)) - freqtrade.create_trade() + freqtrade.create_trades() trades = Trade.query.all() nb_trades = len(trades) assert nb_trades == 1 @@ -1706,7 +1705,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade, value=(True, False)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True @@ -1738,7 +1737,7 @@ def test_handle_trade_experimental( freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True @@ -1766,7 +1765,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, patch_get_signal(freqtrade) # Create trade and sell it - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2106,7 +2105,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2152,7 +2151,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2201,7 +2200,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2258,7 +2257,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, freqtrade.strategy.order_types['stoploss_on_exchange'] = True patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() Trade.session = MagicMock() @@ -2305,7 +2304,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2357,7 +2356,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() freqtrade.process_maybe_execute_sell(trade) assert trade @@ -2409,7 +2408,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2456,7 +2455,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2512,7 +2511,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2543,7 +2542,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2574,7 +2573,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( sell_flag=False, sell_type=SellType.NONE)) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2605,7 +2604,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2635,7 +2634,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2667,7 +2666,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert freqtrade.handle_trade(trade) is False @@ -2720,7 +2719,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2778,7 +2777,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2841,7 +2840,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2900,7 +2899,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -3157,7 +3156,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, whitelist = deepcopy(default_conf['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade is not None @@ -3191,7 +3190,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o # Save state of current whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade is None @@ -3297,7 +3296,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade From a4ab42560f9494b69432addeaab5cf8eb4dc3b82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:16:59 +0200 Subject: [PATCH 175/269] improve docstring for create_trades --- freqtrade/freqtradebot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e67a8687f..e2c88376e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -258,9 +258,10 @@ class FreqtradeBot(object): def create_trades(self) -> bool: """ - Checks the implemented trading indicator(s) for a randomly picked pair, - if one pair triggers the buy_signal a new trade record gets created - :return: True if a trade object has been created and persisted, False otherwise + Checks the implemented trading strategy for buy-signals, using the active pair whitelist. + If a pair triggers the buy_signal a new trade record gets created. + Checks pairs as long as the open trade count is below `max_open_trades`. + :return: True if at least one trade has been created. """ interval = self.strategy.ticker_interval whitelist = copy.deepcopy(self.active_pair_whitelist) From d6f5f6b7ba0577b65b22216cac558a565a68051b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:21:15 +0200 Subject: [PATCH 176/269] Add test with preexisting trades --- freqtrade/tests/test_freqtradebot.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index d74615d6b..868cda4df 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -718,6 +718,33 @@ def test_create_trades_multiple_trades(default_conf, ticker, assert len(trades) == max_open +def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + default_conf['max_open_trades'] = 4 + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + buy=MagicMock(return_value={'id': "12355555"}), + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Create 2 existing trades + freqtrade.execute_buy('ETH/BTC', default_conf['stake_amount']) + freqtrade.execute_buy('NEO/BTC', default_conf['stake_amount']) + + assert len(Trade.get_open_trades()) == 2 + + # Create 2 new trades using create_trades + assert freqtrade.create_trades() + + trades = Trade.get_open_trades() + assert len(trades) == 4 + + def test_process_trade_creation(default_conf, ticker, limit_buy_order, markets, fee, mocker, caplog) -> None: patch_RPCManager(mocker) From 4da2bfefb73d7a9158e2979f941a3c9209c5a581 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 09:36:28 +0200 Subject: [PATCH 177/269] Improve docstring for some downloading methods --- freqtrade/exchange/exchange.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0e23cf8b8..33b250955 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -551,6 +551,11 @@ class Exchange(object): """ Gets candle history using asyncio and returns the list of candles. Handles all async doing. + Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. + :param pair: Pair to download + :param ticker_interval: Interval to get + :param since_ms: Timestamp in milliseconds to get history from + :returns List of tickers """ return asyncio.get_event_loop().run_until_complete( self._async_get_history(pair=pair, ticker_interval=ticker_interval, @@ -585,6 +590,9 @@ class Exchange(object): def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]: """ Refresh in-memory ohlcv asyncronously and set `_klines` with the result + Loops asyncroneously over pair_list and dowloads all pairs async (semi-parallel). + :param pair_list: List of 2 element tuples containing pair,interval to refresh + :return: Returns a List of ticker-dataframes. """ logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list)) From 06fa07e73e998b208c8af7b2ea9d26f36d36fed6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 10:07:14 +0200 Subject: [PATCH 178/269] Move parse_timerange to TimeRange class --- freqtrade/configuration/arguments.py | 58 +------------------------ freqtrade/configuration/timerange.py | 65 ++++++++++++++++++++++++++++ freqtrade/tests/test_arguments.py | 26 +---------- freqtrade/tests/test_timerange.py | 28 ++++++++++++ 4 files changed, 95 insertions(+), 82 deletions(-) create mode 100644 freqtrade/configuration/timerange.py create mode 100644 freqtrade/tests/test_timerange.py diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index dd5a4290e..c129a7e47 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -2,10 +2,8 @@ This module contains the argument manager class """ import argparse -import re -from typing import List, NamedTuple, Optional +from typing import List, Optional -import arrow from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade import constants @@ -43,18 +41,6 @@ ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) -class TimeRange(NamedTuple): - """ - NamedTuple defining timerange inputs. - [start/stop]type defines if [start/stop]ts shall be used. - if *type is None, don't use corresponding startvalue. - """ - starttype: Optional[str] = None - stoptype: Optional[str] = None - startts: int = 0 - stopts: int = 0 - - class Arguments(object): """ Arguments Class. Manage the arguments received by the cli @@ -133,45 +119,3 @@ class Arguments(object): ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) - - @staticmethod - def parse_timerange(text: Optional[str]) -> TimeRange: - """ - Parse the value of the argument --timerange to determine what is the range desired - :param text: value from --timerange - :return: Start and End range period - """ - if text is None: - return TimeRange(None, None, 0, 0) - syntax = [(r'^-(\d{8})$', (None, 'date')), - (r'^(\d{8})-$', ('date', None)), - (r'^(\d{8})-(\d{8})$', ('date', 'date')), - (r'^-(\d{10})$', (None, 'date')), - (r'^(\d{10})-$', ('date', None)), - (r'^(\d{10})-(\d{10})$', ('date', 'date')), - (r'^(-\d+)$', (None, 'line')), - (r'^(\d+)-$', ('line', None)), - (r'^(\d+)-(\d+)$', ('index', 'index'))] - for rex, stype in syntax: - # Apply the regular expression to text - match = re.match(rex, text) - if match: # Regex has matched - rvals = match.groups() - index = 0 - start: int = 0 - stop: int = 0 - if stype[0]: - starts = rvals[index] - if stype[0] == 'date' and len(starts) == 8: - start = arrow.get(starts, 'YYYYMMDD').timestamp - else: - start = int(starts) - index += 1 - if stype[1]: - stops = rvals[index] - if stype[1] == 'date' and len(stops) == 8: - stop = arrow.get(stops, 'YYYYMMDD').timestamp - else: - stop = int(stops) - return TimeRange(stype[0], stype[1], start, stop) - raise Exception('Incorrect syntax for timerange "%s"' % text) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py new file mode 100644 index 000000000..b44704682 --- /dev/null +++ b/freqtrade/configuration/timerange.py @@ -0,0 +1,65 @@ +""" +This module contains the argument manager class +""" +import re +from typing import Optional + +import arrow + + +class TimeRange(): + """ + object defining timerange inputs. + [start/stop]type defines if [start/stop]ts shall be used. + if *type is None, don't use corresponding startvalue. + """ + + def __init__(self, starttype: Optional[str], stoptype: Optional[str], + startts: int, stopts: int): + + self.starttype: Optional[str] = starttype + self.stoptype: Optional[str] = stoptype + self.startts: int = startts + self.stopts: int = stopts + + @staticmethod + def parse_timerange(text: Optional[str]): + """ + Parse the value of the argument --timerange to determine what is the range desired + :param text: value from --timerange + :return: Start and End range period + """ + if text is None: + return TimeRange(None, None, 0, 0) + syntax = [(r'^-(\d{8})$', (None, 'date')), + (r'^(\d{8})-$', ('date', None)), + (r'^(\d{8})-(\d{8})$', ('date', 'date')), + (r'^-(\d{10})$', (None, 'date')), + (r'^(\d{10})-$', ('date', None)), + (r'^(\d{10})-(\d{10})$', ('date', 'date')), + (r'^(-\d+)$', (None, 'line')), + (r'^(\d+)-$', ('line', None)), + (r'^(\d+)-(\d+)$', ('index', 'index'))] + for rex, stype in syntax: + # Apply the regular expression to text + match = re.match(rex, text) + if match: # Regex has matched + rvals = match.groups() + index = 0 + start: int = 0 + stop: int = 0 + if stype[0]: + starts = rvals[index] + if stype[0] == 'date' and len(starts) == 8: + start = arrow.get(starts, 'YYYYMMDD').timestamp + else: + start = int(starts) + index += 1 + if stype[1]: + stops = rvals[index] + if stype[1] == 'date' and len(stops) == 8: + stop = arrow.get(stops, 'YYYYMMDD').timestamp + else: + stop = int(stops) + return TimeRange(stype[0], stype[1], start, stop) + raise Exception('Incorrect syntax for timerange "%s"' % text) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index bf744f72b..2cb7ff6d7 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,7 +3,7 @@ import argparse import pytest -from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import Arguments from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME from freqtrade.configuration.cli_options import check_int_positive @@ -86,30 +86,6 @@ def test_parse_args_strategy_path_invalid() -> None: Arguments(['--strategy-path'], '').get_parsed_arg() -def test_parse_timerange_incorrect() -> None: - assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200') - assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-') - assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500') - - assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-') - assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522') - timerange = Arguments.parse_timerange('20100522-20150730') - assert timerange == TimeRange('date', 'date', 1274486400, 1438214400) - - # Added test for unix timestamp - BTC genesis date - assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-') - assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000') - timerange = Arguments.parse_timerange('1231006505-1233360000') - assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange - - # TODO: Find solution for the following case (passing timestamp in ms) - timerange = Arguments.parse_timerange('1231006505000-1233360000000') - assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange - - with pytest.raises(Exception, match=r'Incorrect syntax.*'): - Arguments.parse_timerange('-') - - def test_parse_args_backtesting_invalid() -> None: with pytest.raises(SystemExit, match=r'2'): Arguments(['backtesting --ticker-interval'], '').get_parsed_arg() diff --git a/freqtrade/tests/test_timerange.py b/freqtrade/tests/test_timerange.py new file mode 100644 index 000000000..6599472fb --- /dev/null +++ b/freqtrade/tests/test_timerange.py @@ -0,0 +1,28 @@ +# pragma pylint: disable=missing-docstring, C0103 +import pytest + +from freqtrade.configuration import TimeRange + + +def test_parse_timerange_incorrect() -> None: + assert TimeRange(None, 'line', 0, -200) == TimeRange.parse_timerange('-200') + assert TimeRange('line', None, 200, 0) == TimeRange.parse_timerange('200-') + assert TimeRange('index', 'index', 200, 500) == TimeRange.parse_timerange('200-500') + + assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-') + assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522') + timerange = TimeRange.parse_timerange('20100522-20150730') + assert timerange == TimeRange('date', 'date', 1274486400, 1438214400) + + # Added test for unix timestamp - BTC genesis date + assert TimeRange('date', None, 1231006505, 0) == TimeRange.parse_timerange('1231006505-') + assert TimeRange(None, 'date', 0, 1233360000) == TimeRange.parse_timerange('-1233360000') + timerange = TimeRange.parse_timerange('1231006505-1233360000') + assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange + + # TODO: Find solution for the following case (passing timestamp in ms) + timerange = TimeRange.parse_timerange('1231006505000-1233360000000') + assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange + + with pytest.raises(Exception, match=r'Incorrect syntax.*'): + TimeRange.parse_timerange('-') From 51c3a31bb58c01696634360b5547fa09b3fc0341 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 10:07:32 +0200 Subject: [PATCH 179/269] Correct imports and calls to parse_timerange --- freqtrade/configuration/__init__.py | 3 ++- freqtrade/edge/__init__.py | 4 ++-- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/edge_cli.py | 4 ++-- freqtrade/optimize/hyperopt.py | 4 ++-- freqtrade/plot/plotting.py | 4 ++-- freqtrade/tests/data/test_btanalysis.py | 4 ++-- freqtrade/tests/test_plotting.py | 6 +++--- scripts/download_backtest_data.py | 2 +- 9 files changed, 18 insertions(+), 17 deletions(-) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 548b508a7..7b476d173 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,2 +1,3 @@ -from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401 +from freqtrade.configuration.arguments import Arguments # noqa: F401 +from freqtrade.configuration.timerange import TimeRange # noqa: F401 from freqtrade.configuration.configuration import Configuration # noqa: F401 diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 7085663d6..2d3097ec4 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -10,7 +10,7 @@ import utils_find_1st as utf1st from pandas import DataFrame from freqtrade import constants, OperationalException -from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.strategy.interface import SellType @@ -75,7 +75,7 @@ class Edge(): self._stoploss_range_step ) - self._timerange: TimeRange = Arguments.parse_timerange("%s-" % arrow.now().shift( + self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift( days=-1 * self._since_number_of_days).format('YYYYMMDD')) self.fee = self.exchange.get_fee() diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 252175269..8f40a6582 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -12,7 +12,7 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from freqtrade import OperationalException -from freqtrade.configuration import Arguments +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes @@ -404,7 +404,7 @@ class Backtesting(object): logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - timerange = Arguments.parse_timerange(None if self.config.get( + timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) data = history.load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 8d1fa381b..7e0d60843 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -9,7 +9,7 @@ from tabulate import tabulate from freqtrade import constants from freqtrade.edge import Edge -from freqtrade.configuration import Arguments +from freqtrade.configuration import TimeRange from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver @@ -41,7 +41,7 @@ class EdgeCli(object): self.edge = Edge(config, self.exchange, self.strategy) self.edge._refresh_pairs = self.config.get('refresh_pairs', False) - self.timerange = Arguments.parse_timerange(None if self.config.get( + self.timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) self.edge._timerange = self.timerange diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 550f13e28..772b4a10f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -20,7 +20,7 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade.configuration import Arguments +from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting # Import IHyperOptLoss to allow users import from this file @@ -310,7 +310,7 @@ class Hyperopt(Backtesting): ) def start(self) -> None: - timerange = Arguments.parse_timerange(None if self.config.get( + timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) data = load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index d03d3ae53..947b3003c 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional import pandas as pd -from freqtrade.configuration import Arguments +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import (combine_tickers_with_mean, create_cum_profit, load_trades) @@ -42,7 +42,7 @@ def init_plotscript(config): pairs = config["exchange"]["pair_whitelist"] # Set timerange to use - timerange = Arguments.parse_timerange(config.get("timerange")) + timerange = TimeRange.parse_timerange(config.get("timerange")) tickers = history.load_data( datadir=Path(str(config.get("datadir"))), diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index bad8db66f..cf8cae566 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -4,7 +4,7 @@ import pytest from arrow import Arrow from pandas import DataFrame, to_datetime -from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, combine_tickers_with_mean, create_cum_profit, @@ -121,7 +121,7 @@ def test_combine_tickers_with_mean(): def test_create_cum_profit(): filename = make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) - timerange = Arguments.parse_timerange("20180110-20180112") + timerange = TimeRange.parse_timerange("20180110-20180112") df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', datadir=None, timerange=timerange) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index bfdd72215..cd72160f8 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import plotly.graph_objects as go from plotly.subplots import make_subplots -from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.plot.plotting import (add_indicators, add_profit, @@ -222,7 +222,7 @@ def test_generate_plot_file(mocker, caplog): def test_add_profit(): filename = history.make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) - timerange = Arguments.parse_timerange("20180110-20180112") + timerange = TimeRange.parse_timerange("20180110-20180112") df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m', datadir=None, timerange=timerange) @@ -242,7 +242,7 @@ def test_add_profit(): def test_generate_profit_graph(): filename = history.make_testdata_path(None) / "backtest-result_test.json" trades = load_backtest_data(filename) - timerange = Arguments.parse_timerange("20180110-20180112") + timerange = TimeRange.parse_timerange("20180110-20180112") pairs = ["POWR/BTC", "XLM/BTC"] tickers = history.load_data(datadir=None, diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 580592294..f77ad7422 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -105,7 +105,7 @@ if not pairs or args.pairs_file: timerange = TimeRange() if args.days: time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") - timerange = arguments.parse_timerange(f'{time_since}-') + timerange = TimeRange.parse_timerange(f'{time_since}-') logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') From 84baef922c131f17532ecf3637ee618a5e2e57d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 10:14:54 +0200 Subject: [PATCH 180/269] Rename get_history to get_historic_ohlcv --- freqtrade/data/history.py | 9 +++++---- freqtrade/exchange/exchange.py | 14 +++++++------- freqtrade/tests/data/test_history.py | 10 +++++----- freqtrade/tests/exchange/test_exchange.py | 4 ++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index f600615df..899c6d0c8 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -252,10 +252,11 @@ def download_pair_history(datadir: Optional[Path], logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') # Default since_ms to 30 days if nothing is given - new_data = exchange.get_history(pair=pair, ticker_interval=ticker_interval, - since_ms=since_ms if since_ms - else - int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000) + new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, + since_ms=since_ms if since_ms + else + int(arrow.utcnow().shift( + days=-30).float_timestamp) * 1000) data.extend(new_data) logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 33b250955..65cb409dc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -546,8 +546,8 @@ class Exchange(object): logger.info("returning cached ticker-data for %s", pair) return self._cached_ticker[pair] - def get_history(self, pair: str, ticker_interval: str, - since_ms: int) -> List: + def get_historic_ohlcv(self, pair: str, ticker_interval: str, + since_ms: int) -> List: """ Gets candle history using asyncio and returns the list of candles. Handles all async doing. @@ -558,12 +558,12 @@ class Exchange(object): :returns List of tickers """ return asyncio.get_event_loop().run_until_complete( - self._async_get_history(pair=pair, ticker_interval=ticker_interval, - since_ms=since_ms)) + self._async_get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, + since_ms=since_ms)) - async def _async_get_history(self, pair: str, - ticker_interval: str, - since_ms: int) -> List: + async def _async_get_historic_ohlcv(self, pair: str, + ticker_interval: str, + since_ms: int) -> List: one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit logger.debug( diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 00f4738f7..4ba65e470 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -80,7 +80,7 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json') _backup_file(file, copy_file=True) history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC']) @@ -96,7 +96,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau """ Test load_pair_history() with 1 min ticker """ - mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list) exchange = get_patched_exchange(mocker, default_conf) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') @@ -266,7 +266,7 @@ def test_load_cached_data_for_updating(mocker) -> None: def test_download_pair_history(ticker_history_list, mocker, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list) exchange = get_patched_exchange(mocker, default_conf) file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json') @@ -319,7 +319,7 @@ def test_download_pair_history2(mocker, default_conf) -> None: [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] ] json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) - mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m') download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m') @@ -327,7 +327,7 @@ def test_download_pair_history2(mocker, default_conf) -> None: def test_download_backtesting_data_exception(ticker_history, mocker, caplog, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_history', + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', side_effect=Exception('File Error')) exchange = get_patched_exchange(mocker, default_conf) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e8a7201f1..ed80edfce 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -996,7 +996,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_history(default_conf, mocker, caplog, exchange_name): +def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) tick = [ [ @@ -1017,7 +1017,7 @@ def test_get_history(default_conf, mocker, caplog, exchange_name): # one_call calculation * 1.8 should do 2 calls since = 5 * 60 * 500 * 1.8 print(f"since = {since}") - ret = exchange.get_history(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) assert exchange._async_get_candle_history.call_count == 2 # Returns twice the above tick From 096a6426dbf1142c5443371f9569e4f51870d148 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 10:22:54 +0200 Subject: [PATCH 181/269] Override equality operator --- freqtrade/configuration/timerange.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index b44704682..f980b71ea 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -14,14 +14,19 @@ class TimeRange(): if *type is None, don't use corresponding startvalue. """ - def __init__(self, starttype: Optional[str], stoptype: Optional[str], - startts: int, stopts: int): + def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None, + startts: int = 0, stopts: int = 0): self.starttype: Optional[str] = starttype self.stoptype: Optional[str] = stoptype self.startts: int = startts self.stopts: int = stopts + def __eq__(self, other): + """Override the default Equals behavior""" + return (self.starttype == other.starttype and self.stoptype == other.stoptype + and self.startts == other.startts and self.stopts == other.stopts) + @staticmethod def parse_timerange(text: Optional[str]): """ From 0ffb184ebad3631320e7f3279746e441d3c591a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 20:45:24 +0200 Subject: [PATCH 182/269] Change some docstrings and formatting from history --- freqtrade/data/history.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index f600615df..363495796 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -57,10 +57,8 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: return tickerlist[start_index:stop_index] -def load_tickerdata_file( - datadir: Optional[Path], pair: str, - ticker_interval: str, - timerange: Optional[TimeRange] = None) -> Optional[list]: +def load_tickerdata_file(datadir: Optional[Path], pair: str, ticker_interval: str, + timerange: Optional[TimeRange] = None) -> Optional[list]: """ Load a pair from file, either .json.gz or .json :return: tickerlist or None if unsuccesful @@ -68,7 +66,7 @@ def load_tickerdata_file( filename = pair_data_filename(datadir, pair, ticker_interval) pairdata = misc.file_load_json(filename) if not pairdata: - return None + return [] if timerange: pairdata = trim_tickerlist(pairdata, timerange) @@ -182,6 +180,7 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str, Optional[int]]: """ Load cached data and choose what part of the data should be updated + Only used by download_pair_history(). """ since_ms = None From 91d1061c7304a598caa1be3a38c055530920956d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 20:48:42 +0200 Subject: [PATCH 183/269] Abstract tickerdata storing --- freqtrade/data/history.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 363495796..849b882b2 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -73,6 +73,15 @@ def load_tickerdata_file(datadir: Optional[Path], pair: str, ticker_interval: st return pairdata +def store_tickerdata_file(datadir: Optional[Path], pair: str, + ticker_interval: str, data: list, is_zip: bool = False): + """ + Stores tickerdata to file + """ + filename = pair_data_filename(datadir, pair, ticker_interval) + misc.file_dump_json(filename, data, is_zip=is_zip) + + def load_pair_history(pair: str, ticker_interval: str, datadir: Optional[Path], @@ -175,7 +184,7 @@ def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) return filename -def load_cached_data_for_updating(filename: Path, ticker_interval: str, +def load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ @@ -194,12 +203,10 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str, since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file - if filename.is_file(): - with open(filename, "rt") as file: - data = misc.json_load(file) - # remove the last item, could be incomplete candle - if data: - data.pop() + data = load_tickerdata_file(datadir, pair, ticker_interval, TimeRange) + # remove the last item, could be incomplete candle + if data: + data.pop() else: data = [] @@ -238,14 +245,12 @@ def download_pair_history(datadir: Optional[Path], ) try: - filename = pair_data_filename(datadir, pair, ticker_interval) - logger.info( f'Download history data for pair: "{pair}", interval: {ticker_interval} ' f'and store in {datadir}.' ) - data, since_ms = load_cached_data_for_updating(filename, ticker_interval, timerange) + data, since_ms = load_cached_data_for_updating(datadir, pair, ticker_interval, timerange) logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') @@ -260,7 +265,7 @@ def download_pair_history(datadir: Optional[Path], logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) logger.debug("New End: %s", misc.format_ms_time(data[-1][0])) - misc.file_dump_json(filename, data) + store_tickerdata_file(datadir, pair, ticker_interval, data=data) return True except Exception as e: From 9d3322df8c00d4d18f4b182ff0d494055c002013 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 20:49:13 +0200 Subject: [PATCH 184/269] Adapt history-tests to new load_cached_data header --- freqtrade/tests/data/test_history.py | 40 +++++++--------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 00f4738f7..a06c5aa23 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -178,53 +178,41 @@ def test_load_cached_data_for_updating(mocker) -> None: # timeframe starts earlier than the cached data # should fully update data timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == [] assert start_ts == test_data[0][0] - 1000 # same with 'line' timeframe num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120 - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - TimeRange(None, 'line', 0, -num_lines)) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', TimeRange(None, 'line', 0, -num_lines)) assert data == [] assert start_ts < test_data[0][0] - 1 # timeframe starts in the center of the cached data # should return the chached data w/o the last item timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] # same with 'line' timeframe num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30 timerange = TimeRange(None, 'line', 0, -num_lines) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] # timeframe starts after the chached data # should return the chached data w/o the last item timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] # same with 'line' timeframe num_lines = 30 timerange = TimeRange(None, 'line', 0, -num_lines) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] @@ -232,35 +220,27 @@ def test_load_cached_data_for_updating(mocker) -> None: # should return the chached data w/o the last item num_lines = 30 timerange = TimeRange(None, 'line', 0, -num_lines) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] # no datafile exist # should return timestamp start time timerange = TimeRange('date', None, now_ts - 10000, 0) - data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange) assert data == [] assert start_ts == (now_ts - 10000) * 1000 # same with 'line' timeframe num_lines = 30 timerange = TimeRange(None, 'line', 0, -num_lines) - data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange) assert data == [] assert start_ts == (now_ts - num_lines * 60) * 1000 # no datafile exist, no timeframe is set # should return an empty array and None - data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), - '1m', - None) + data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', None) assert data == [] assert start_ts is None From b2a22f1afb4e10d635c33623acddd22a8dc4d9ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 21:39:53 +0200 Subject: [PATCH 185/269] Fix samll errors --- freqtrade/data/history.py | 4 ++-- freqtrade/tests/data/test_history.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 849b882b2..c6d731afa 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -184,7 +184,7 @@ def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) return filename -def load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str, +def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_interval: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ @@ -203,7 +203,7 @@ def load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file - data = load_tickerdata_file(datadir, pair, ticker_interval, TimeRange) + data = load_tickerdata_file(datadir, pair, ticker_interval, timerange) # remove the last item, could be incomplete candle if data: data.pop() diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index a06c5aa23..164ebe01a 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -184,7 +184,8 @@ def test_load_cached_data_for_updating(mocker) -> None: # same with 'line' timeframe num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120 - data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', TimeRange(None, 'line', 0, -num_lines)) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', + TimeRange(None, 'line', 0, -num_lines)) assert data == [] assert start_ts < test_data[0][0] - 1 From f3e6bcb20c166bc7287caced4e3559604a39f0c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 06:35:50 +0200 Subject: [PATCH 186/269] Avoid using negative indexes --- freqtrade/data/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index c6d731afa..5471767f6 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -43,7 +43,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: start_index += 1 if timerange.stoptype == 'line': - start_index = len(tickerlist) + timerange.stopts + start_index = max(len(tickerlist) + timerange.stopts, 0) if timerange.stoptype == 'index': stop_index = timerange.stopts elif timerange.stoptype == 'date': From 11790fbf0174f565a1a86dda2eb4fb283ea5d93d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 06:37:26 +0200 Subject: [PATCH 187/269] Fix typos in docstrings --- freqtrade/exchange/exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 65cb409dc..a2558d3d8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -589,9 +589,9 @@ class Exchange(object): def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]: """ - Refresh in-memory ohlcv asyncronously and set `_klines` with the result - Loops asyncroneously over pair_list and dowloads all pairs async (semi-parallel). - :param pair_list: List of 2 element tuples containing pair,interval to refresh + Refresh in-memory ohlcv asynchronously and set `_klines` with the result + Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). + :param pair_list: List of 2 element tuples containing pair, interval to refresh :return: Returns a List of ticker-dataframes. """ logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list)) @@ -640,7 +640,7 @@ class Exchange(object): async def _async_get_candle_history(self, pair: str, ticker_interval: str, since_ms: Optional[int] = None) -> Tuple[str, str, List]: """ - Asyncronously gets candle histories using fetch_ohlcv + Asynchronously gets candle histories using fetch_ohlcv returns tuple: (pair, ticker_interval, ohlcv_list) """ try: From f5e437d8c789dc9e383ae2dddfe1c249ffbc956c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 06:59:45 +0200 Subject: [PATCH 188/269] Change create_trade to create_trades for new test --- freqtrade/tests/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index d6d547c29..24d070d2d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2611,7 +2611,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mock patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2630,7 +2630,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mock # reinit - should buy other pair. caplog.clear() - freqtrade.create_trade() + freqtrade.create_trades() assert log_has(f"Pair {trade.pair} is currently locked.", caplog) From fd77f699dfbda3bf72ea9c42de13abc46531809a Mon Sep 17 00:00:00 2001 From: Ashton Honnecke Date: Thu, 15 Aug 2019 10:41:02 -0600 Subject: [PATCH 189/269] f the string --- freqtrade/configuration/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 17ad37d6a..046a4320b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -290,7 +290,7 @@ class Configuration(object): if not self.runmode: # Handle real mode, infer dry/live from config self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE - logger.info("Runmode set to {self.runmode}.") + logger.info(f"Runmode set to {self.runmode}.") config.update({'runmode': self.runmode}) From a94a89086f7b37638a4eec19c93482db9d05a731 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 20:09:00 +0200 Subject: [PATCH 190/269] Don't forward timerange to load_ticker_file when loading cached data for updating. We always want to get all data, not just a fraction (we would end up overwriting the non-loaded part of the data). --- freqtrade/data/history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 5471767f6..14749925f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -203,7 +203,8 @@ def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_int since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file - data = load_tickerdata_file(datadir, pair, ticker_interval, timerange) + # Intentionally don't pass timerange in - since we need to load the full dataset. + data = load_tickerdata_file(datadir, pair, ticker_interval) # remove the last item, could be incomplete candle if data: data.pop() From 12677f2d42f8d3f789cd84b42f693bb142c6f9ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 20:13:19 +0200 Subject: [PATCH 191/269] Adjust docstring to match functioning of load_cached_data --- freqtrade/data/history.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 14749925f..af7d8cc7c 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -188,7 +188,9 @@ def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_int timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ - Load cached data and choose what part of the data should be updated + Load cached data to download more data. + If timerange is passed in, checks wether data from an before the stored data will be downloaded. + If that's the case than what's available should be completely overwritten. Only used by download_pair_history(). """ From 69eff890496df3cc374ae1f8eb260aa2d421db52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 20:28:32 +0200 Subject: [PATCH 192/269] Improve comment in test_history to explain what is tested --- freqtrade/tests/data/test_history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 164ebe01a..f238046d7 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -210,7 +210,8 @@ def test_load_cached_data_for_updating(mocker) -> None: assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] - # same with 'line' timeframe + # Try loading last 30 lines. + # Not supported by load_cached_data_for_updating, we always need to get the full data. num_lines = 30 timerange = TimeRange(None, 'line', 0, -num_lines) data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) From 4fa92ec0fa468f759d1d8b602cfbcbfd881b35de Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 15 Aug 2019 21:39:04 +0300 Subject: [PATCH 193/269] hyperopt: --print-json option added --- freqtrade/configuration/arguments.py | 2 +- freqtrade/configuration/cli_options.py | 6 +++ freqtrade/configuration/configuration.py | 3 ++ freqtrade/optimize/hyperopt.py | 53 +++++++++++++++++------- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index c129a7e47..926d02f8f 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -22,7 +22,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", - "print_colorized", "hyperopt_jobs", + "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", "hyperopt_continue", "hyperopt_loss"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index b098fa8bc..84686d1e6 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -198,6 +198,12 @@ AVAILABLE_CLI_OPTIONS = { action='store_false', default=True, ), + "print_json": Arg( + '--print-json', + help='Print best result detailization in JSON format.', + action='store_true', + default=False, + ), "hyperopt_jobs": Arg( '-j', '--job-workers', help='The number of concurrently running jobs for hyperoptimization ' diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 587c8757e..eaba0b4da 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -242,6 +242,9 @@ class Configuration(object): else: config.update({'print_colorized': True}) + self._args_to_config(config, argname='print_json', + logstring='Parameter --print-json detected ...') + self._args_to_config(config, argname='hyperopt_jobs', logstring='Parameter -j/--job-workers detected: {}') diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 772b4a10f..7664608ce 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -8,11 +8,14 @@ import logging import os import sys +from collections import OrderedDict from operator import itemgetter from pathlib import Path from pprint import pprint from typing import Any, Dict, List, Optional +import rapidjson + from colorama import init as colorama_init from colorama import Fore, Style from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count @@ -133,22 +136,44 @@ class Hyperopt(Backtesting): results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] params = best_result['params'] - log_str = self.format_results_logstring(best_result) print(f"\nBest result:\n\n{log_str}\n") - if self.has_space('buy'): - print('Buy hyperspace params:') - pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, - indent=4) - if self.has_space('sell'): - print('Sell hyperspace params:') - pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, - indent=4) - if self.has_space('roi'): - print("ROI table:") - pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) - if self.has_space('stoploss'): - print(f"Stoploss: {params.get('stoploss')}") + + if self.config.get('print_json'): + result_dict = {} + if self.has_space('buy') or self.has_space('sell'): + result_dict['params'] = {} + if self.has_space('buy'): + result_dict['params'].update({p.name: params.get(p.name) + for p in self.hyperopt_space('buy')}) + if self.has_space('sell'): + result_dict['params'].update({p.name: params.get(p.name) + for p in self.hyperopt_space('sell')}) + if self.has_space('roi'): + min_roi = self.custom_hyperopt.generate_roi_table(params) + # Convert keys in min_roi dict to strings because + # rapidjson cannot dump dicts with integer keys... + # OrderedDict is used to keep the numeric order of the items + # in the dict. + min_roi = OrderedDict((str(k),v) for k,v in min_roi.items()) + result_dict['minimal_roi'] = min_roi + if self.has_space('stoploss'): + result_dict['stoploss'] = params.get('stoploss') + print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) + else: + if self.has_space('buy'): + print('Buy hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, + indent=4) + if self.has_space('sell'): + print('Sell hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, + indent=4) + if self.has_space('roi'): + print("ROI table:") + pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) + if self.has_space('stoploss'): + print(f"Stoploss: {params.get('stoploss')}") def log_results(self, results) -> None: """ From e525275d102872a68c323000d0ab5d1fd69f5740 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 15 Aug 2019 23:13:46 +0300 Subject: [PATCH 194/269] make flake and mypy happy --- freqtrade/optimize/hyperopt.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7664608ce..05b12e653 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -140,23 +140,23 @@ class Hyperopt(Backtesting): print(f"\nBest result:\n\n{log_str}\n") if self.config.get('print_json'): - result_dict = {} + result_dict: Dict = {} if self.has_space('buy') or self.has_space('sell'): result_dict['params'] = {} if self.has_space('buy'): result_dict['params'].update({p.name: params.get(p.name) - for p in self.hyperopt_space('buy')}) + for p in self.hyperopt_space('buy')}) if self.has_space('sell'): result_dict['params'].update({p.name: params.get(p.name) - for p in self.hyperopt_space('sell')}) + for p in self.hyperopt_space('sell')}) if self.has_space('roi'): - min_roi = self.custom_hyperopt.generate_roi_table(params) # Convert keys in min_roi dict to strings because # rapidjson cannot dump dicts with integer keys... # OrderedDict is used to keep the numeric order of the items # in the dict. - min_roi = OrderedDict((str(k),v) for k,v in min_roi.items()) - result_dict['minimal_roi'] = min_roi + result_dict['minimal_roi'] = OrderedDict( + (str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items() + ) if self.has_space('stoploss'): result_dict['stoploss'] = params.get('stoploss') print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) From 2a842778e36bdef97af38ba955b0baad71cdd624 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 16 Aug 2019 00:49:49 +0300 Subject: [PATCH 195/269] tests added --- freqtrade/tests/optimize/test_hyperopt.py | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 0317bb37d..b77ea1c84 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -618,3 +618,69 @@ def test_continue_hyperopt(mocker, default_conf, caplog): assert unlinkmock.call_count == 0 assert log_has(f"Continuing on previous hyperopt results.", caplog) + + +def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + parallel = mocker.patch( + 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) + ) + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, + 'print_json': True, + }) + + hyperopt = Hyperopt(default_conf) + hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + hyperopt.start() + + parallel.assert_called_once() + + out, err = capsys.readouterr() + assert '{"params":{"mfi-value":null,"fastd-value":null,"adx-value":null,"rsi-value":null,"mfi-enabled":null,"fastd-enabled":null,"adx-enabled":null,"rsi-enabled":null,"trigger":null,"sell-mfi-value":null,"sell-fastd-value":null,"sell-adx-value":null,"sell-rsi-value":null,"sell-mfi-enabled":null,"sell-fastd-enabled":null,"sell-adx-enabled":null,"sell-rsi-enabled":null,"sell-trigger":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501 + + +def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None: + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + parallel = mocker.patch( + 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) + ) + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'roi stoploss', + 'hyperopt_jobs': 1, + 'print_json': True, + }) + + hyperopt = Hyperopt(default_conf) + hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + hyperopt.start() + + parallel.assert_called_once() + + out, err = capsys.readouterr() + assert '{"minimal_roi":{},"stoploss":null}' in out From b94f3e80c40d4d817a32e13e63f467763d2a9eed Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 16 Aug 2019 04:20:12 +0300 Subject: [PATCH 196/269] tests fixed --- freqtrade/tests/optimize/test_hyperopt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index b77ea1c84..1c4e2445c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -621,6 +621,7 @@ def test_continue_hyperopt(mocker, default_conf, caplog): def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', @@ -651,9 +652,13 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: out, err = capsys.readouterr() assert '{"params":{"mfi-value":null,"fastd-value":null,"adx-value":null,"rsi-value":null,"mfi-enabled":null,"fastd-enabled":null,"adx-enabled":null,"rsi-enabled":null,"trigger":null,"sell-mfi-value":null,"sell-fastd-value":null,"sell-adx-value":null,"sell-rsi-value":null,"sell-mfi-enabled":null,"sell-fastd-enabled":null,"sell-adx-enabled":null,"sell-rsi-enabled":null,"sell-trigger":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501 + assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None: + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', @@ -684,3 +689,6 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> out, err = capsys.readouterr() assert '{"minimal_roi":{},"stoploss":null}' in out + assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 From 8d206f83083fe880293c035ff6019ccc1cb50113 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 06:29:19 +0200 Subject: [PATCH 197/269] Fix wrong warning box --- docs/strategy-customization.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 15f44955b..0d08bdd02 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -309,8 +309,10 @@ if self.dp: dataframe['best_bid'] = ob['bids'][0][0] dataframe['best_ask'] = ob['asks'][0][0] ``` -!Warning The order book is not part of the historic data which means backtesting and hyperopt will not work if this - method is used. + +!!! Warning + The order book is not part of the historic data which means backtesting and hyperopt will not work if this + method is used. #### Available Pairs From 53db382695b236d404a9557b05200e1bd9616e12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 10:19:06 +0200 Subject: [PATCH 198/269] Update dockerfile python version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7a0298719..8677b54de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.3-slim-stretch +FROM python:3.7.4-slim-stretch RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev \ From 09286d49182fb98553dd2d137fc85b3d6b742a50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 13:04:07 +0200 Subject: [PATCH 199/269] file_dump_json accepts Path - so we should feed it that --- freqtrade/misc.py | 8 ++++---- freqtrade/optimize/backtesting.py | 12 ++++++------ freqtrade/tests/optimize/test_backtesting.py | 5 +++-- freqtrade/tests/test_misc.py | 5 +++-- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 05946e008..d01d6a254 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -5,11 +5,11 @@ import gzip import logging import re from datetime import datetime +from pathlib import Path import numpy as np import rapidjson - logger = logging.getLogger(__name__) @@ -39,7 +39,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: return dates.dt.to_pydatetime() -def file_dump_json(filename, data, is_zip=False) -> None: +def file_dump_json(filename: Path, data, is_zip=False) -> None: """ Dump JSON data into a file :param filename: file to create @@ -49,8 +49,8 @@ def file_dump_json(filename, data, is_zip=False) -> None: logger.info(f'dumping json to "{filename}"') if is_zip: - if not filename.endswith('.gz'): - filename = filename + '.gz' + if filename.suffix != '.gz': + filename = filename.with_suffix('.gz') with gzip.open(filename, 'w') as fp: rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) else: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 252175269..d321affeb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -190,7 +190,7 @@ class Backtesting(object): return tabulate(tabular_data, headers=headers, # type: ignore floatfmt=floatfmt, tablefmt="pipe") - def _store_backtest_result(self, recordfilename: str, results: DataFrame, + def _store_backtest_result(self, recordfilename: Path, results: DataFrame, strategyname: Optional[str] = None) -> None: records = [(t.pair, t.profit_percent, t.open_time.timestamp(), @@ -201,10 +201,10 @@ class Backtesting(object): if records: if strategyname: # Inject strategyname to filename - recname = Path(recordfilename) - recordfilename = str(Path.joinpath( - recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix)) - logger.info('Dumping backtest results to %s', recordfilename) + recordfilename = Path.joinpath( + recordfilename.parent, + f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix) + logger.info(f'Dumping backtest results to {recordfilename}') file_dump_json(recordfilename, records) def _get_ticker_list(self, processed) -> Dict[str, DataFrame]: @@ -458,7 +458,7 @@ class Backtesting(object): for strategy, results in all_results.items(): if self.config.get('export', False): - self._store_backtest_result(self.config['exportfilename'], results, + self._store_backtest_result(Path(self.config['exportfilename']), results, strategy if len(self.strategylist) > 1 else None) print(f"Result for strategy {strategy}") diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 9ed7e7296..02e9a9c28 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -2,6 +2,7 @@ import math import random +from pathlib import Path from unittest.mock import MagicMock import numpy as np @@ -785,10 +786,10 @@ def test_backtest_record(default_conf, fee, mocker): # reset test to test with strategy name names = [] records = [] - backtesting._store_backtest_result("backtest-result.json", results, "DefStrat") + backtesting._store_backtest_result(Path("backtest-result.json"), results, "DefStrat") assert len(results) == 4 # Assert file_dump_json was only called once - assert names == ['backtest-result-DefStrat.json'] + assert names == [Path('backtest-result-DefStrat.json')] records = records[0] # Ensure records are of correct type assert len(records) == 4 diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 1a6b2a92d..c55083e64 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring,C0103 import datetime +from pathlib import Path from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe @@ -34,12 +35,12 @@ def test_datesarray_to_datetimearray(ticker_history_list): def test_file_dump_json(mocker) -> None: file_open = mocker.patch('freqtrade.misc.open', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock()) - file_dump_json('somefile', [1, 2, 3]) + file_dump_json(Path('somefile'), [1, 2, 3]) assert file_open.call_count == 1 assert json_dump.call_count == 1 file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock()) - file_dump_json('somefile', [1, 2, 3], True) + file_dump_json(Path('somefile'), [1, 2, 3], True) assert file_open.call_count == 1 assert json_dump.call_count == 1 From 91886120a7d17071a4663cd39c36d378b7d563c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:37:10 +0200 Subject: [PATCH 200/269] use nargs for --pairs argument --- freqtrade/configuration/cli_options.py | 6 ++++-- freqtrade/constants.py | 1 - freqtrade/plot/plotting.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 84686d1e6..d39013737 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -254,7 +254,8 @@ AVAILABLE_CLI_OPTIONS = { # Script options "pairs": Arg( '-p', '--pairs', - help='Show profits for only these pairs. Pairs are comma-separated.', + help='Show profits for only these pairs. Pairs are space-separated.', + nargs='+', ), # Download data "pairs_file": Arg( @@ -276,9 +277,10 @@ AVAILABLE_CLI_OPTIONS = { "timeframes": Arg( '-t', '--timeframes', help=f'Specify which tickers to download. Space-separated list. ' - f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.', + f'Default: `1m 5m`.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], + default=['1m', '5m'], nargs='+', ), "erase": Arg( diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9b73adcfe..fbf44dec8 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -23,7 +23,6 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 -DEFAULT_DOWNLOAD_TICKER_INTERVALS = '1m 5m' TICKER_INTERVALS = [ '1m', '3m', '5m', '15m', '30m', diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 947b3003c..e6da581a4 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -37,7 +37,7 @@ def init_plotscript(config): strategy = StrategyResolver(config).strategy if "pairs" in config: - pairs = config["pairs"].split(',') + pairs = config["pairs"] else: pairs = config["exchange"]["pair_whitelist"] From 05deb9e09bdccd0c19904ed2687cc6bd8f2bf29f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:42:44 +0200 Subject: [PATCH 201/269] Migrate download-script logic to utils.py --- freqtrade/configuration/arguments.py | 19 ++++++-- freqtrade/tests/test_arguments.py | 18 ++++---- freqtrade/utils.py | 66 ++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 16 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 926d02f8f..8fa16318a 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -30,7 +30,7 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_LIST_EXCHANGES = ["print_one_column"] -ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] +ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", @@ -40,6 +40,8 @@ ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) +NO_CONF_REQURIED = ["start_download_data"] + class Arguments(object): """ @@ -75,7 +77,10 @@ class Arguments(object): # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) - if not self._no_default_config and parsed_arg.config is None: + # Allow no-config for certain commands (like downloading / plotting) + if (not self._no_default_config and parsed_arg.config is None + and not (hasattr(parsed_arg, 'func') + and parsed_arg.func.__name__ in NO_CONF_REQURIED)): parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg @@ -93,7 +98,7 @@ class Arguments(object): :return: None """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge - from freqtrade.utils import start_list_exchanges + from freqtrade.utils import start_download_data, start_list_exchanges subparsers = self.parser.add_subparsers(dest='subparser') @@ -119,3 +124,11 @@ class Arguments(object): ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) + + # Add download-data subcommand + download_data_cmd = subparsers.add_parser( + 'download-data', + help='Download backtesting data.' + ) + download_data_cmd.set_defaults(func=start_download_data) + self._build_args(optionlist=ARGS_DOWNLOADER, parser=download_data_cmd) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 2cb7ff6d7..31ab9dea8 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -50,10 +50,10 @@ def test_parse_args_verbose() -> None: def test_common_scripts_options() -> None: - arguments = Arguments(['-p', 'ETH/BTC'], '') - arguments._build_args(ARGS_DOWNLOADER) - args = arguments._parse_args() - assert args.pairs == 'ETH/BTC' + args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC'], '').get_parsed_arg() + + assert args.pairs == ['ETH/BTC', 'XRP/BTC'] + assert hasattr(args, "func") def test_parse_args_version() -> None: @@ -135,14 +135,14 @@ def test_parse_args_hyperopt_custom() -> None: def test_download_data_options() -> None: args = [ - '--pairs-file', 'file_with_pairs', '--datadir', 'datadir/directory', + 'download-data', + '--pairs-file', 'file_with_pairs', '--days', '30', '--exchange', 'binance' ] - arguments = Arguments(args, '') - arguments._build_args(ARGS_DOWNLOADER) - args = arguments._parse_args() + args = Arguments(args, '').get_parsed_arg() + assert args.pairs_file == 'file_with_pairs' assert args.datadir == 'datadir/directory' assert args.days == 30 @@ -162,7 +162,7 @@ def test_plot_dataframe_options() -> None: assert pargs.indicators1 == "sma10,sma100" assert pargs.indicators2 == "macd,fastd,fastk" assert pargs.plot_limit == 30 - assert pargs.pairs == "UNITTEST/BTC" + assert pargs.pairs == ["UNITTEST/BTC"] def test_check_int_positive() -> None: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index d550ef43c..d2770ba1a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,11 +1,16 @@ import logging +import sys from argparse import Namespace +from pathlib import Path from typing import Any, Dict -from freqtrade.configuration import Configuration -from freqtrade.exchange import available_exchanges -from freqtrade.state import RunMode +import arrow +from freqtrade.configuration import Configuration, TimeRange +from freqtrade.data.history import download_pair_history +from freqtrade.exchange import available_exchanges +from freqtrade.resolvers import ExchangeResolver +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -17,7 +22,7 @@ def setup_utils_configuration(args: Namespace, method: RunMode) -> Dict[str, Any :return: Configuration """ configuration = Configuration(args, method) - config = configuration.load_config() + config = configuration.get_config() config['exchange']['dry_run'] = True # Ensure we do not use Exchange credentials @@ -39,3 +44,56 @@ def start_list_exchanges(args: Namespace) -> None: else: print(f"Exchanges supported by ccxt and available for Freqtrade: " f"{', '.join(available_exchanges())}") + + +def start_download_data(args: Namespace) -> None: + """ + Download data based + """ + config = setup_utils_configuration(args, RunMode.OTHER) + + timerange = TimeRange() + if 'days' in config: + time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d") + timerange = TimeRange.parse_timerange(f'{time_since}-') + + dl_path = Path(config['datadir']) + logger.info(f'About to download pairs: {config["pairs"]}, ' + f'intervals: {config["timeframes"]} to {dl_path}') + + pairs_not_available = [] + + try: + # Init exchange + exchange = ExchangeResolver(config['exchange']['name'], config).exchange + + for pair in config["pairs"]: + if pair not in exchange._api.markets: + pairs_not_available.append(pair) + logger.info(f"Skipping pair {pair}...") + continue + for ticker_interval in config["timeframes"]: + pair_print = pair.replace('/', '_') + filename = f'{pair_print}-{ticker_interval}.json' + dl_file = dl_path.joinpath(filename) + if args.erase and dl_file.exists(): + logger.info( + f'Deleting existing data for pair {pair}, interval {ticker_interval}.') + dl_file.unlink() + + logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') + download_pair_history(datadir=dl_path, exchange=exchange, + pair=pair, ticker_interval=str(ticker_interval), + timerange=timerange) + + except KeyboardInterrupt: + sys.exit("SIGINT received, aborting ...") + + finally: + if pairs_not_available: + logger.info( + f"Pairs [{','.join(pairs_not_available)}] not available " + f"on exchange {config['exchange']['name']}.") + + # configuration.resolve_pairs_list() + print(config) From 8655e521d7c64812ffda7f69e95e4959a6fcf6f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:53:46 +0200 Subject: [PATCH 202/269] Adapt some tests --- freqtrade/tests/test_arguments.py | 2 +- freqtrade/tests/test_main.py | 4 +++- freqtrade/tests/test_plotting.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 31ab9dea8..601f41e63 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -4,7 +4,7 @@ import argparse import pytest from freqtrade.configuration import Arguments -from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME +from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME from freqtrade.configuration.cli_options import check_int_positive diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index d8ec532b0..409025a3c 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,7 +1,7 @@ # pragma pylint: disable=missing-docstring from copy import deepcopy -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import pytest @@ -21,6 +21,7 @@ def test_parse_args_backtesting(mocker) -> None: further argument parsing is done in test_arguments.py """ backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock()) + backtesting_mock.__name__ = PropertyMock("start_backtesting") # it's sys.exit(0) at the end of backtesting with pytest.raises(SystemExit): main(['backtesting']) @@ -36,6 +37,7 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock()) + hyperopt_mock.__name__ = PropertyMock("start_hyperopt") # it's sys.exit(0) at the end of hyperopt with pytest.raises(SystemExit): main(['hyperopt']) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index cd72160f8..94d40ab84 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -50,7 +50,7 @@ def test_init_plotscript(default_conf, mocker): assert "pairs" in ret assert "strategy" in ret - default_conf['pairs'] = "POWR/BTC,XLM/BTC" + default_conf['pairs'] = ["POWR/BTC", "XLM/BTC"] ret = init_plotscript(default_conf) assert "tickers" in ret assert "POWR/BTC" in ret["tickers"] From 3c15e3ebddd07d14ba085f4d226d5bea2d7a97a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:56:38 +0200 Subject: [PATCH 203/269] Default load minimal config --- freqtrade/configuration/configuration.py | 13 +++++++++++++ freqtrade/constants.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index c95246fc0..e24ace34b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -52,6 +52,9 @@ class Configuration(object): # Keep this method as staticmethod, so it can be used from interactive environments config: Dict[str, Any] = {} + if not files: + return constants.MINIMAL_CONFIG + # We expect here a list of config filenames for path in files: logger.info(f'Using config: {path} ...') @@ -276,6 +279,16 @@ class Configuration(object): self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') + self._args_to_config(config, argname='timeframes', + logstring='timeframes --timeframes: {}') + + self._args_to_config(config, argname='days', + logstring='Detected --days: {}') + + if "exchange" in self.args and self.args.exchange: + config['exchange']['name'] = self.args.exchange + logger.info(f"Using exchange {config['exchange']['name']}") + def _process_runmode(self, config: Dict[str, Any]) -> None: if not self.runmode: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index fbf44dec8..b73a723eb 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -38,6 +38,20 @@ SUPPORTED_FIAT = [ "BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT" ] +MINIMAL_CONFIG = { + 'stake_currency': '', + 'dry_run': True, + 'exchange': { + 'name': '', + 'key': '', + 'secret': '', + 'pair_whitelist': [], + 'ccxt_async_config': { + 'enableRateLimit': True, + } + } +} + # Required json-schema for user specified config CONF_SCHEMA = { 'type': 'object', From 4e308a1a3e4aef76c809e53f1d60953399fb8655 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:56:57 +0200 Subject: [PATCH 204/269] Resolve pairlist in configuration --- freqtrade/configuration/configuration.py | 37 ++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index e24ace34b..ed12b6501 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -90,6 +90,11 @@ class Configuration(object): self._process_runmode(config) + # Check if the exchange set by the user is supported + check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) + + self._resolve_pairs_list(config) + return config def _process_logging_options(self, config: Dict[str, Any]) -> None: @@ -150,9 +155,6 @@ class Configuration(object): if 'sd_notify' in self.args and self.args.sd_notify: config['internals'].update({'sd_notify': True}) - # Check if the exchange set by the user is supported - check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) - def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load datadir configuration: @@ -348,3 +350,32 @@ class Configuration(object): logger.info(logstring.format(config[argname])) if deprecated_msg: warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning) + + def _resolve_pairs_list(self, config: Dict[str, Any]) -> None: + """ + Helper for download script. + Takes first found: + * -p (pairs argument) + * --pairs-file + * whitelist from config + """ + + if "pairs" in self.args and self.args.pairs: + return + + if "pairs_file" in self.args and self.args.pairs_file: + pairs_file = self.args.pairs_file + logger.info(f'Reading pairs file "{pairs_file}".') + # Download pairs from the pairs file if no config is specified + # or if pairs file is specified explicitely + if not pairs_file.exists(): + OperationalException(f'No pairs file found with path "{pairs_file}".') + + # with pairs_file.open() as file: + # pairs = list(set(json.load(file))) + + # pairs.sort() + + if "config" in self.args: + logger.info("Using pairlist from configuration.") + config['pairs'] = config.get('exchange', {}).get('pair_whitelist') From 219d0b7fb016f51f90f5778941d6aba72f907910 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 15:27:33 +0200 Subject: [PATCH 205/269] Adjust documentation to removed download-script --- docs/backtesting.md | 67 +++++++++++++----------- freqtrade/configuration/configuration.py | 3 ++ freqtrade/data/history.py | 2 +- freqtrade/tests/data/test_history.py | 4 +- freqtrade/utils.py | 4 +- 5 files changed, 43 insertions(+), 37 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 7e9f7ff53..f666c5b49 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -3,9 +3,43 @@ This page explains how to validate your strategy performance by using Backtesting. +## Getting data for backtesting / hyperopt + +To download backtesting data (candles / OHLCV), we recommend using the `freqtrade download-data` command. + +If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes. +Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. + +Alternatively, a `pairs.json` file can be used. + +If you are using Binance for example: + +- create a directory `user_data/data/binance` and copy `pairs.json` in that directory. +- update the `pairs.json` to contain the currency pairs you are interested in. + +```bash +mkdir -p user_data/data/binance +cp freqtrade/tests/testdata/pairs.json user_data/data/binance +``` + +Then run: + +```bash +freqtrade download-data --exchange binance +``` + +This will download ticker data for all the currency pairs you defined in `pairs.json`. + +- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. +- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.) +- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. +- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days). +- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. +- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. + ## Test your strategy with Backtesting -Now you have good Buy and Sell strategies, you want to test it against +Now you have good Buy and Sell strategies and some historic data, you want to test it against real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). @@ -109,37 +143,6 @@ The full timerange specification: - Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600` -#### Downloading new set of ticker data - -To download new set of backtesting ticker data, you can use a download script. - -If you are using Binance for example: - -- create a directory `user_data/data/binance` and copy `pairs.json` in that directory. -- update the `pairs.json` to contain the currency pairs you are interested in. - -```bash -mkdir -p user_data/data/binance -cp freqtrade/tests/testdata/pairs.json user_data/data/binance -``` - -Then run: - -```bash -python scripts/download_backtest_data.py --exchange binance -``` - -This will download ticker data for all the currency pairs you defined in `pairs.json`. - -- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. -- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`. -- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. -- To download ticker data for only 10 days, use `--days 10`. -- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. -- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options. - -For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands). - ## Understand the backtesting result The most important in the backtesting is to understand the result. diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index ed12b6501..f7c393b60 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -281,6 +281,9 @@ class Configuration(object): self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') + self._args_to_config(config, argname='erase', + logstring='Erase detected. Deleting existing data.') + self._args_to_config(config, argname='timeframes', logstring='timeframes --timeframes: {}') diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 899c6d0c8..c7b3a28b0 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -122,7 +122,7 @@ def load_pair_history(pair: str, else: logger.warning( f'No history data for pair: "{pair}", interval: {ticker_interval}. ' - 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'Use --refresh-pairs-cached option or `freqtrade download-data` ' 'script to download the data' ) return None diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 4ba65e470..ea56b4bec 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -74,7 +74,7 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: assert ld is None assert log_has( 'No history data for pair: "UNITTEST/BTC", interval: 7m. ' - 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'Use --refresh-pairs-cached option or `freqtrade download-data` ' 'script to download the data', caplog ) @@ -109,7 +109,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau assert os.path.isfile(file) is False assert log_has( 'No history data for pair: "MEME/BTC", interval: 1m. ' - 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'Use --refresh-pairs-cached option or `freqtrade download-data` ' 'script to download the data', caplog ) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index d2770ba1a..7ccbae81a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -48,7 +48,7 @@ def start_list_exchanges(args: Namespace) -> None: def start_download_data(args: Namespace) -> None: """ - Download data based + Download data (former download_backtest_data.py script) """ config = setup_utils_configuration(args, RunMode.OTHER) @@ -76,7 +76,7 @@ def start_download_data(args: Namespace) -> None: pair_print = pair.replace('/', '_') filename = f'{pair_print}-{ticker_interval}.json' dl_file = dl_path.joinpath(filename) - if args.erase and dl_file.exists(): + if config.get("erase") and dl_file.exists(): logger.info( f'Deleting existing data for pair {pair}, interval {ticker_interval}.') dl_file.unlink() From 89257832d721cb06c7e2f80f860f8006fc856fc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 15:27:59 +0200 Subject: [PATCH 206/269] Don't use internal _API methods --- freqtrade/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 7ccbae81a..002d89738 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -68,7 +68,7 @@ def start_download_data(args: Namespace) -> None: exchange = ExchangeResolver(config['exchange']['name'], config).exchange for pair in config["pairs"]: - if pair not in exchange._api.markets: + if pair not in exchange.markets: pairs_not_available.append(pair) logger.info(f"Skipping pair {pair}...") continue From b2c215029d4937223b88b39a7d37c2373896be0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 15:28:11 +0200 Subject: [PATCH 207/269] Add tests for download_data entrypoint --- freqtrade/tests/test_utils.py | 51 ++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index a12b709d7..8d0e76cde 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -1,8 +1,11 @@ -from freqtrade.utils import setup_utils_configuration, start_list_exchanges -from freqtrade.tests.conftest import get_args -from freqtrade.state import RunMode - import re +from pathlib import Path +from unittest.mock import MagicMock, PropertyMock + +from freqtrade.state import RunMode +from freqtrade.tests.conftest import get_args, log_has, patch_exchange +from freqtrade.utils import (setup_utils_configuration, start_download_data, + start_list_exchanges) def test_setup_utils_configuration(): @@ -40,3 +43,43 @@ def test_list_exchanges(capsys): assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out) assert re.search(r"^binance$", captured.out, re.MULTILINE) assert re.search(r"^bittrex$", captured.out, re.MULTILINE) + + +def test_download_data(mocker, markets, caplog): + dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch.object(Path, "unlink", MagicMock()) + + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--erase" + ] + start_download_data(get_args(args)) + + assert dl_mock.call_count == 4 + assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog) + assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) + + +def test_download_data_no_markets(mocker, caplog): + dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) + ) + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC" + ] + start_download_data(get_args(args)) + + assert dl_mock.call_count == 0 + assert log_has("Skipping pair ETH/BTC...", caplog) + assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog) From 132f28ad44be07194ee68cda69a488c75d1b7b8b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 15:52:59 +0200 Subject: [PATCH 208/269] Add tests to correctly load / override pair-lists --- freqtrade/configuration/configuration.py | 21 ++++-- freqtrade/tests/test_configuration.py | 88 ++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index f7c393b60..cb698544d 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -4,6 +4,7 @@ This module contains the configuration class import logging import warnings from argparse import Namespace +from pathlib import Path from typing import Any, Callable, Dict, List, Optional from freqtrade import OperationalException, constants @@ -12,7 +13,7 @@ from freqtrade.configuration.create_datadir import create_datadir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.configuration.load_config import load_config_file from freqtrade.loggers import setup_logging -from freqtrade.misc import deep_merge_dicts +from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -363,22 +364,28 @@ class Configuration(object): * whitelist from config """ - if "pairs" in self.args and self.args.pairs: + if "pairs" in config: return if "pairs_file" in self.args and self.args.pairs_file: - pairs_file = self.args.pairs_file + pairs_file = Path(self.args.pairs_file) logger.info(f'Reading pairs file "{pairs_file}".') # Download pairs from the pairs file if no config is specified # or if pairs file is specified explicitely if not pairs_file.exists(): - OperationalException(f'No pairs file found with path "{pairs_file}".') + raise OperationalException(f'No pairs file found with path "{pairs_file}".') - # with pairs_file.open() as file: - # pairs = list(set(json.load(file))) + config['pairs'] = json_load(pairs_file) - # pairs.sort() + config['pairs'].sort() + return if "config" in self.args: logger.info("Using pairlist from configuration.") config['pairs'] = config.get('exchange', {}).get('pair_whitelist') + else: + # Fall back to /dl_path/pairs.json + pairs_file = Path(config['datadir']) / "pairs.json" + if pairs_file.exists(): + config['pairs'] = json_load(pairs_file) + diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 8cbd02ece..c351b9b72 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -704,3 +704,91 @@ def test_load_config_default_subkeys(all_conf, keys) -> None: validate_config_schema(all_conf) assert subkey in all_conf[key] assert all_conf[key][subkey] == keys[2] + + +def test_pairlist_resolving(): + arglist = [ + 'download-data', + '--pairs', 'ETH/BTC', 'XRP/BTC', + '--exchange', 'binance' + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['name'] == 'binance' + + +def test_pairlist_resolving_with_config(mocker, default_conf): + patched_configuration_load_config_file(mocker, default_conf) + arglist = [ + '--config', 'config.json', + 'download-data', + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert config['pairs'] == default_conf['exchange']['pair_whitelist'] + assert config['exchange']['name'] == default_conf['exchange']['name'] + + # Override pairs + arglist = [ + '--config', 'config.json', + 'download-data', + '--pairs', 'ETH/BTC', 'XRP/BTC', + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['name'] == default_conf['exchange']['name'] + + +def test_pairlist_resolving_with_config_pl(mocker, default_conf): + patched_configuration_load_config_file(mocker, default_conf) + load_mock = mocker.patch("freqtrade.configuration.configuration.json_load", + MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + + arglist = [ + '--config', 'config.json', + 'download-data', + '--pairs-file', 'pairs.json', + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert load_mock.call_count == 1 + assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['name'] == default_conf['exchange']['name'] + + +def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): + patched_configuration_load_config_file(mocker, default_conf) + mocker.patch("freqtrade.configuration.configuration.json_load", + MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + + arglist = [ + '--config', 'config.json', + 'download-data', + '--pairs-file', 'pairs.json', + ] + + args = Arguments(arglist, '').get_parsed_arg() + + with pytest.raises(OperationalException, match=r"No pairs file found with path.*"): + configuration = Configuration(args) + configuration.get_config() From c9207bcc0070bd574bc301f984955bbf3cdda0cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 16:01:30 +0200 Subject: [PATCH 209/269] Remove blank line at end --- freqtrade/configuration/configuration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index cb698544d..c51153f4b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -388,4 +388,3 @@ class Configuration(object): pairs_file = Path(config['datadir']) / "pairs.json" if pairs_file.exists(): config['pairs'] = json_load(pairs_file) - From 29c56f4447ad53cb1d9dc157227be4ea09b4ab29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 06:47:53 +0200 Subject: [PATCH 210/269] Replace download_backtest_data script with warning message --- scripts/download_backtest_data.py | 145 ++---------------------------- 1 file changed, 5 insertions(+), 140 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index f77ad7422..496f83c7d 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -1,144 +1,9 @@ -#!/usr/bin/env python3 -""" -This script generates json files with pairs history data -""" -import arrow -import json import sys -from pathlib import Path -from typing import Any, Dict, List -from freqtrade.configuration import Arguments, TimeRange -from freqtrade.configuration import Configuration -from freqtrade.configuration.arguments import ARGS_DOWNLOADER -from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.load_config import load_config_file -from freqtrade.data.history import download_pair_history -from freqtrade.exchange import Exchange -from freqtrade.misc import deep_merge_dicts -import logging +print("This script has been integrated into freqtrade " + "and it's functionality is available by calling `freqtrade download-data`.") +print("Please check the documentation on https://www.freqtrade.io/en/latest/backtesting/ " + "for details.") -logger = logging.getLogger('download_backtest_data') - -DEFAULT_DL_PATH = 'user_data/data' - -# Do not read the default config if config is not specified -# in the command line options explicitely -arguments = Arguments(sys.argv[1:], 'Download backtest data', - no_default_config=True) -arguments._build_args(optionlist=ARGS_DOWNLOADER) -args = arguments._parse_args() - -# Use bittrex as default exchange -exchange_name = args.exchange or 'bittrex' - -pairs: List = [] - -configuration = Configuration(args) -config: Dict[str, Any] = {} - -if args.config: - # Now expecting a list of config filenames here, not a string - for path in args.config: - logger.info(f"Using config: {path}...") - # Merge config options, overwriting old values - config = deep_merge_dicts(load_config_file(path), config) - - config['stake_currency'] = '' - # Ensure we do not use Exchange credentials - config['exchange']['dry_run'] = True - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - pairs = config['exchange']['pair_whitelist'] - - if config.get('ticker_interval'): - timeframes = args.timeframes or [config.get('ticker_interval')] - else: - timeframes = args.timeframes or ['1m', '5m'] - -else: - config = { - 'stake_currency': '', - 'dry_run': True, - 'exchange': { - 'name': exchange_name, - 'key': '', - 'secret': '', - 'pair_whitelist': [], - 'ccxt_async_config': { - 'enableRateLimit': True, - 'rateLimit': 200 - } - } - } - timeframes = args.timeframes or ['1m', '5m'] - -configuration._process_logging_options(config) - -if args.config and args.exchange: - logger.warning("The --exchange option is ignored, " - "using exchange settings from the configuration file.") - -# Check if the exchange set by the user is supported -check_exchange(config) - -configuration._process_datadir_options(config) - -dl_path = Path(config['datadir']) - -pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json') - -if not pairs or args.pairs_file: - logger.info(f'Reading pairs file "{pairs_file}".') - # Download pairs from the pairs file if no config is specified - # or if pairs file is specified explicitely - if not pairs_file.exists(): - sys.exit(f'No pairs file found with path "{pairs_file}".') - - with pairs_file.open() as file: - pairs = list(set(json.load(file))) - - pairs.sort() - -timerange = TimeRange() -if args.days: - time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") - timerange = TimeRange.parse_timerange(f'{time_since}-') - -logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') - -pairs_not_available = [] - -try: - # Init exchange - exchange = Exchange(config) - - for pair in pairs: - if pair not in exchange._api.markets: - pairs_not_available.append(pair) - logger.info(f"Skipping pair {pair}...") - continue - for ticker_interval in timeframes: - pair_print = pair.replace('/', '_') - filename = f'{pair_print}-{ticker_interval}.json' - dl_file = dl_path.joinpath(filename) - if args.erase and dl_file.exists(): - logger.info( - f'Deleting existing data for pair {pair}, interval {ticker_interval}.') - dl_file.unlink() - - logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') - download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, ticker_interval=str(ticker_interval), - timerange=timerange) - -except KeyboardInterrupt: - sys.exit("SIGINT received, aborting ...") - -finally: - if pairs_not_available: - logger.info( - f"Pairs [{','.join(pairs_not_available)}] not available " - f"on exchange {config['exchange']['name']}.") +sys.exit(1) From f7d5280f47ced954a57536cde7dc1cc6561f7203 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 06:48:34 +0200 Subject: [PATCH 211/269] Replace ARGS_DOWNLOADER with ARGS_DOWNLOAD_DATA --- freqtrade/configuration/arguments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 8fa16318a..c45e3d7ba 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -30,7 +30,7 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_LIST_EXCHANGES = ["print_one_column"] -ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] +ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", @@ -131,4 +131,4 @@ class Arguments(object): help='Download backtesting data.' ) download_data_cmd.set_defaults(func=start_download_data) - self._build_args(optionlist=ARGS_DOWNLOADER, parser=download_data_cmd) + self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd) From a53e9e3a98cf440d1f14a9d55d907a4139e46b97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 06:58:38 +0200 Subject: [PATCH 212/269] improve tests for download_module --- freqtrade/tests/test_utils.py | 50 +++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index 8d0e76cde..003b3b286 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -2,6 +2,8 @@ import re from pathlib import Path from unittest.mock import MagicMock, PropertyMock +import pytest + from freqtrade.state import RunMode from freqtrade.tests.conftest import get_args, log_has, patch_exchange from freqtrade.utils import (setup_utils_configuration, start_download_data, @@ -58,15 +60,41 @@ def test_download_data(mocker, markets, caplog): "download-data", "--exchange", "binance", "--pairs", "ETH/BTC", "XRP/BTC", - "--erase" + "--erase", ] start_download_data(get_args(args)) assert dl_mock.call_count == 4 + assert dl_mock.call_args[1]['timerange'].starttype is None + assert dl_mock.call_args[1]['timerange'].stoptype is None assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog) assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) +def test_download_data_days(mocker, markets, caplog): + dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch.object(Path, "unlink", MagicMock()) + + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--days", "20", + ] + + start_download_data(get_args(args)) + + assert dl_mock.call_count == 4 + assert dl_mock.call_args[1]['timerange'].starttype == 'date' + + assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) + + def test_download_data_no_markets(mocker, caplog): dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) patch_exchange(mocker) @@ -76,10 +104,28 @@ def test_download_data_no_markets(mocker, caplog): args = [ "download-data", "--exchange", "binance", - "--pairs", "ETH/BTC", "XRP/BTC" + "--pairs", "ETH/BTC", "XRP/BTC", ] start_download_data(get_args(args)) assert dl_mock.call_count == 0 assert log_has("Skipping pair ETH/BTC...", caplog) assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog) + + +def test_download_data_keyboardInterrupt(mocker, caplog, markets): + dl_mock = mocker.patch('freqtrade.utils.download_pair_history', + MagicMock(side_effect=KeyboardInterrupt)) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + ] + with pytest.raises(SystemExit): + start_download_data(get_args(args)) + + assert dl_mock.call_count == 1 From 7a79b292e46dfab4e19e4635c8720bac8cde7676 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 07:05:42 +0200 Subject: [PATCH 213/269] Fix bug in pairs fallback resolving --- freqtrade/configuration/configuration.py | 3 ++- freqtrade/tests/test_configuration.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index c51153f4b..676b0c594 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -380,7 +380,7 @@ class Configuration(object): config['pairs'].sort() return - if "config" in self.args: + if "config" in self.args and self.args.config: logger.info("Using pairlist from configuration.") config['pairs'] = config.get('exchange', {}).get('pair_whitelist') else: @@ -388,3 +388,4 @@ class Configuration(object): pairs_file = Path(config['datadir']) / "pairs.json" if pairs_file.exists(): config['pairs'] = json_load(pairs_file) + config['pairs'].sort() diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index c351b9b72..b6e8a76d9 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -792,3 +792,21 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): with pytest.raises(OperationalException, match=r"No pairs file found with path.*"): configuration = Configuration(args) configuration.get_config() + + +def test_pairlist_resolving_fallback(mocker): + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch("freqtrade.configuration.configuration.json_load", + MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) + arglist = [ + 'download-data', + '--exchange', 'binance' + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['name'] == 'binance' From 08fa5136e11843c7ea3da2c464f1a0e77d921f03 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 07:19:46 +0200 Subject: [PATCH 214/269] use copy of minimal_config ... --- freqtrade/configuration/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 676b0c594..75319ac47 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -54,7 +54,7 @@ class Configuration(object): config: Dict[str, Any] = {} if not files: - return constants.MINIMAL_CONFIG + return constants.MINIMAL_CONFIG.copy() # We expect here a list of config filenames for path in files: From 84a0f9ea42518d9e1dced4e027044365ec00a01c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 17 Aug 2019 11:43:36 +0300 Subject: [PATCH 215/269] get_pair_dataframe helper method added --- freqtrade/data/dataprovider.py | 46 ++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b87589df7..e806f5aa7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -44,36 +44,50 @@ class DataProvider(): def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: """ - get ohlcv data for the given pair as DataFrame - Please check `available_pairs` to verify which pairs are currently cached. + Get ohlcv data for the given pair as DataFrame + Please check `self.available_pairs` to verify which pairs are currently cached. :param pair: pair to get the data for - :param ticker_interval: ticker_interval to get pair for - :param copy: copy dataframe before returning. - Use false only for RO operations (where the dataframe is not modified) + :param ticker_interval: ticker interval to get data for + :param copy: copy dataframe before returning if True. + Use False only for read-only operations (where the dataframe is not modified) """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - if ticker_interval: - pairtick = (pair, ticker_interval) - else: - pairtick = (pair, self._config['ticker_interval']) + pairtick = (pair, ticker_interval or self._config['ticker_interval']) + if pairtick in self.available_pairs: + return self._exchange.klines(pairtick, copy=copy) - return self._exchange.klines(pairtick, copy=copy) - else: - return DataFrame() + return DataFrame() - def historic_ohlcv(self, pair: str, ticker_interval: str) -> DataFrame: + def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame: """ - get stored historic ohlcv data + Get stored historic ohlcv data :param pair: pair to get the data for - :param ticker_interval: ticker_interval to get pair for + :param ticker_interval: ticker interval to get data for """ return load_pair_history(pair=pair, - ticker_interval=ticker_interval, + ticker_interval=ticker_interval or self._config['ticker_interval'], refresh_pairs=False, datadir=Path(self._config['datadir']) if self._config.get( 'datadir') else None ) + def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame: + """ + Return pair ohlcv data, either live or cached historical -- depending + on the runmode. + :param pair: pair to get the data for + :param ticker_interval: ticker interval to get data for + """ + if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + # Get live ohlcv data. + data = self.ohlcv(pair=pair, ticker_interval=ticker_interval) + else: + # Get historic ohlcv data (cached on disk). + data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval) + if len(data) == 0: + logger.warning(f"No data found for pair {pair}") + return data + def ticker(self, pair: str): """ Return last ticker data From cda912bd8cc060b09168b68194b3a042b8f879ab Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 17 Aug 2019 12:59:27 +0300 Subject: [PATCH 216/269] test added --- freqtrade/tests/data/test_dataprovider.py | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/freqtrade/tests/data/test_dataprovider.py b/freqtrade/tests/data/test_dataprovider.py index 993f0b59b..54aab7052 100644 --- a/freqtrade/tests/data/test_dataprovider.py +++ b/freqtrade/tests/data/test_dataprovider.py @@ -51,6 +51,39 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history): assert historymock.call_args_list[0][1]["ticker_interval"] == "5m" +def test_get_pair_dataframe(mocker, default_conf, ticker_history): + default_conf["runmode"] = RunMode.DRY_RUN + ticker_interval = default_conf["ticker_interval"] + exchange = get_patched_exchange(mocker, default_conf) + exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history + exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history + dp = DataProvider(default_conf, exchange) + assert dp.runmode == RunMode.DRY_RUN + assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) + assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ticker_history + assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty + assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + + # Test with and without parameter + assert dp.get_pair_dataframe("UNITTEST/BTC", + ticker_interval).equals(dp.get_pair_dataframe("UNITTEST/BTC")) + + default_conf["runmode"] = RunMode.LIVE + dp = DataProvider(default_conf, exchange) + assert dp.runmode == RunMode.LIVE + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) + assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + + historymock = MagicMock(return_value=ticker_history) + mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) + default_conf["runmode"] = RunMode.BACKTEST + dp = DataProvider(default_conf, exchange) + assert dp.runmode == RunMode.BACKTEST + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) + # assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + + def test_available_pairs(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) From fce3d7586f320b1fc391d6a52b757f54ed3438d3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2019 15:13:39 +0000 Subject: [PATCH 217/269] Bump pytest from 5.0.1 to 5.1.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.0.1 to 5.1.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.0.1...5.1.0) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 03b37417e..6436c60e4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 mypy==0.720 -pytest==5.0.1 +pytest==5.1.0 pytest-asyncio==0.10.0 pytest-cov==2.7.1 pytest-mock==1.10.4 From 4ce3cc66d5ea41be0bfbf4c6a1a1b64c5117efd7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2019 15:14:01 +0000 Subject: [PATCH 218/269] Bump sqlalchemy from 1.3.6 to 1.3.7 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.6 to 1.3.7. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 651be7611..91da6e45c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.18.1043 -SQLAlchemy==1.3.6 +SQLAlchemy==1.3.7 python-telegram-bot==11.1.0 arrow==0.14.5 cachetools==3.1.1 From e0335705b2a95387f32d692f6417f55ccaf070b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 17:19:02 +0200 Subject: [PATCH 219/269] Add dependabot config yaml --- .dependabot/config.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .dependabot/config.yml diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 000000000..66b91e99f --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,17 @@ +version: 1 + +update_configs: + - package_manager: "python" + directory: "/" + update_schedule: "weekly" + allowed_updates: + - match: + update_type: "all" + target_branch: "develop" + + - package_manager: "docker" + directory: "/" + update_schedule: "daily" + allowed_updates: + - match: + update_type: "all" From 9143ea13adbeeee2fad130debaf1d0ebbd9e8a2c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2019 15:26:07 +0000 Subject: [PATCH 220/269] Bump ccxt from 1.18.1043 to 1.18.1063 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1043 to 1.18.1063. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.18.1043...1.18.1063) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 91da6e45c..4666fc053 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.18.1043 +ccxt==1.18.1063 SQLAlchemy==1.3.7 python-telegram-bot==11.1.0 arrow==0.14.5 From 351740fc80761176891fb7bbe463672c540e8384 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 17:27:14 +0200 Subject: [PATCH 221/269] Change pyup to every month (should ideally not find anything ...) --- .pyup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pyup.yml b/.pyup.yml index b1b721113..7ab3e5eca 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -14,7 +14,7 @@ pin: True # update schedule # default: empty # allowed: "every day", "every week", .. -schedule: "every week" +schedule: "every month" search: False From 0e87cc8c84c72a75d7889f02fb584f46036400b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 19:30:03 +0200 Subject: [PATCH 222/269] Remove pyup.yml --- .pyup.yml | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .pyup.yml diff --git a/.pyup.yml b/.pyup.yml deleted file mode 100644 index 7ab3e5eca..000000000 --- a/.pyup.yml +++ /dev/null @@ -1,37 +0,0 @@ -# autogenerated pyup.io config file -# see https://pyup.io/docs/configuration/ for all available options - -# configure updates globally -# default: all -# allowed: all, insecure, False -update: all - -# configure dependency pinning globally -# default: True -# allowed: True, False -pin: True - -# update schedule -# default: empty -# allowed: "every day", "every week", .. -schedule: "every month" - - -search: False -# Specify requirement files by hand, default is empty -# default: empty -# allowed: list -requirements: - - requirements.txt - - requirements-dev.txt - - requirements-plot.txt - - requirements-common.txt - - -# configure the branch prefix the bot is using -# default: pyup- -branch_prefix: pyup/ - -# allow to close stale PRs -# default: True -close_prs: True From 7fa6d804ce5a7352ec92f6114279acc90118aab6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 19:48:55 +0200 Subject: [PATCH 223/269] Add note explaining how / when docker images are rebuild --- docs/docker.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docker.md b/docs/docker.md index 615d31796..923dec1e2 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -26,6 +26,10 @@ To update the image, simply run the above commands again and restart your runnin Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). +!!! Note Docker image update frequency + The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. + In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. + ### Prepare the configuration files Even though you will use docker, you'll still need some files from the github repository. From 2cffc3228a0e12ec1667c0f17c8ede0bd286a541 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Sun, 11 Aug 2019 12:52:37 -0400 Subject: [PATCH 224/269] split example notebooks --- .gitignore | 9 +- docs/data-analysis.md | 161 +++++++--- .../notebooks/jupyter_snippets_example.ipynb | 289 ++++++++++++++++++ ....ipynb => strategy_analysis_example.ipynb} | 215 ++++++------- 4 files changed, 514 insertions(+), 160 deletions(-) create mode 100644 user_data/notebooks/jupyter_snippets_example.ipynb rename user_data/notebooks/{analysis_example.ipynb => strategy_analysis_example.ipynb} (51%) diff --git a/.gitignore b/.gitignore index 3a9df9852..1664ad7eb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,10 @@ config*.json .hyperopt logfile.txt hyperopt_trials.pickle -user_data/ +user_data/* +!user_data/notebooks +user_data/notebooks/* +!user_data/notebooks/*example.ipynb freqtrade-plot.html freqtrade-profit-plot.html @@ -80,7 +83,7 @@ docs/_build/ target/ # Jupyter Notebook -.ipynb_checkpoints +*.ipynb_checkpoints # pyenv .python-version @@ -94,4 +97,4 @@ target/ .mypy_cache/ #exceptions -!user_data/noteboks/*example.ipynb +!*.gitkeep diff --git a/docs/data-analysis.md b/docs/data-analysis.md index c89353cc8..8d4cc63c0 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -1,30 +1,96 @@ -# Analyzing bot data +# Analyzing bot data with Jupyter notebooks -You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation). +You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/`. -*Pro tip - Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* +## Pro tips -## Example snippets +* See [jupyter.org](https://jupyter.org/documentation) for usage instructions. +* Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* +* Copy the example notebook so your changes don't get clobbered with the next freqtrade update. + +## Fine print + +Some tasks don't work especially well in notebooks. For example, anything using asyncronous exectution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required parameters to functions. + +## Recommended workflow + +| Task | Tool | + --- | --- +Bot operations | CLI +Repetative tasks | shell scripts +Data analysis & visualization | Notebook + +1. Use the CLI to + * download historical data + * run a backtest + * run with real-time data + * export results + +1. Collect these actions in shell scripts + * save complicated commands with arguments + * execute mult-step operations + * automate testing strategies and prepareing data for analysis + +1. Use a notebook to + * import data + * munge and plot to generate insights + +## Example utility snippets for Jupyter notebooks + +### Change directory to root + +Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistant. + +```python +# Change directory +# Modify this cell to insure that the output shows the correct path. +# Define all paths relative to the project root shown in the cell output +import os +from pathlib import Path + +project_root = "somedir/freqtrade" +i=0 +try: + os.chdirdir(project_root) + assert Path('LICENSE').is_file() +except: + while i<4 and (not Path('LICENSE').is_file()): + os.chdir(Path(Path.cwd(), '../')) + i+=1 + project_root = Path.cwd() +print(Path.cwd()) +``` + +### Watch project for changes to code + +This scans the project for changes to code before Jupyter runs cells. + +```python +# Reloads local code changes +%load_ext autoreload +%autoreload 2 +``` + +## Load existing objects into a Jupyter notebook + +These examples assume that you have already generated data using the cli. These examples will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload. ### Load backtest results into a pandas dataframe ```python -from freqtrade.data.btanalysis import load_backtest_data # Load backtest results +from freqtrade.data.btanalysis import load_backtest_data df = load_backtest_data("user_data/backtest_data/backtest-result.json") # Show value-counts per pair df.groupby("pair")["sell_reason"].value_counts() ``` -This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload. - ### Load live trading results into a pandas dataframe ``` python -from freqtrade.data.btanalysis import load_trades_from_db - # Fetch trades from database +from freqtrade.data.btanalysis import load_trades_from_db df = load_trades_from_db("sqlite:///tradesv3.sqlite") # Display results @@ -33,42 +99,61 @@ df.groupby("pair")["sell_reason"].value_counts() ### Load multiple configuration files -This option can be usefull to inspect the results of passing in multiple configs in case of problems +This option can be useful to inspect the results of passing in multiple configs ``` python +# Load config from multiple files from freqtrade.configuration import Configuration config = Configuration.from_files(["config1.json", "config2.json"]) -print(config) + +# Show the config in memory +import json +print(json.dumps(config, indent=1)) ``` -## Strategy debugging example +### Load exchange data to a pandas dataframe + +This loads candle data to a dataframe + +```python +# Load data using values passed to function +from pathlib import Path +from freqtrade.data.history import load_pair_history + +ticker_interval = "5m" +data_location = Path('user_data', 'data', 'bitrex') +pair = "BTC_USDT" +candles = load_pair_history(datadir=data_location, + ticker_interval=ticker_interval, + pair=pair) + +# Confirm success +print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}") +display(candles.head()) +``` + +## Strategy debugging example Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data. -### Import requirements and define variables used in analyses +### Define variables used in analyses + +You can override strategy settings as demonstrated below. ```python -# Imports -from pathlib import Path -import os -from freqtrade.data.history import load_pair_history -from freqtrade.resolvers import StrategyResolver - -# You can override strategy settings as demonstrated below. # Customize these according to your needs. # Define some constants ticker_interval = "5m" # Name of the strategy class -strategy_name = 'AwesomeStrategy' +strategy_name = 'TestStrategy' # Path to user data user_data_dir = 'user_data' # Location of the strategy strategy_location = Path(user_data_dir, 'strategies') # Location of the data data_location = Path(user_data_dir, 'data', 'binance') -# Pair to analyze -# Only use one pair here +# Pair to analyze - Only use one pair here pair = "BTC_USDT" ``` @@ -76,12 +161,16 @@ pair = "BTC_USDT" ```python # Load data using values set above -bt_data = load_pair_history(datadir=Path(data_location), +from pathlib import Path +from freqtrade.data.history import load_pair_history + +candles = load_pair_history(datadir=data_location, ticker_interval=ticker_interval, pair=pair) # Confirm success -print(f"Loaded {len(bt_data)} rows of data for {pair} from {data_location}") +print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}") +display(candles.head()) ``` ### Load and run strategy @@ -90,31 +179,27 @@ print(f"Loaded {len(bt_data)} rows of data for {pair} from {data_location}") ```python # Load strategy using values set above +from freqtrade.resolvers import StrategyResolver strategy = StrategyResolver({'strategy': strategy_name, 'user_data_dir': user_data_dir, 'strategy_path': strategy_location}).strategy # Generate buy/sell signals using strategy -df = strategy.analyze_ticker(bt_data, {'pair': pair}) +df = strategy.analyze_ticker(candles, {'pair': pair}) ``` ### Display the trade details * Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. - -#### Some possible problems - -* Columns with NaN values at the end of the dataframe -* Columns used in `crossed*()` functions with completely different units - -#### Comparison with full backtest - -having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting. - -Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). -The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available. +* Some possible problems + * Columns with NaN values at the end of the dataframe + * Columns used in `crossed*()` functions with completely different units +* Comparison with full backtest + * having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting. + * Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available. ```python + # Report results print(f"Generated {df['buy'].sum()} buy signals") data = df.set_index('date', drop=True) diff --git a/user_data/notebooks/jupyter_snippets_example.ipynb b/user_data/notebooks/jupyter_snippets_example.ipynb new file mode 100644 index 000000000..9d4ae467a --- /dev/null +++ b/user_data/notebooks/jupyter_snippets_example.ipynb @@ -0,0 +1,289 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Analyzing bot data with Jupyter notebooks \n", + "\n", + "You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. \n", + "\n", + "## Pro tips \n", + "\n", + "* See [jupyter.org](https://jupyter.org/documentation) for usage instructions.\n", + "* Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*\n", + "* Copy the example notebook so your changes don't get clobbered with the next freqtrade update.\n", + "\n", + "## Fine print \n", + "\n", + "Some tasks don't work especially well in notebooks. For example, anything using asyncronous exectution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required parameters to functions.\n", + "\n", + "## Recommended workflow \n", + "\n", + "| Task | Tool | \n", + " --- | --- \n", + "Bot operations | CLI \n", + "Repetative tasks | shell scripts\n", + "Data analysis & visualization | Notebook \n", + "\n", + "1. Use the CLI to\n", + " * download historical data\n", + " * run a backtest\n", + " * run with real-time data\n", + " * export results \n", + "\n", + "1. Collect these actions in shell scripts\n", + " * save complicated commands with arguments\n", + " * execute mult-step operations \n", + " * automate testing strategies and prepareing data for analysis\n", + "\n", + "1. Use a notebook to\n", + " * import data\n", + " * munge and plot to generate insights" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example utility snippets for Jupyter notebooks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Change directory to root \n", + "\n", + "Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistant." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change directory\n", + "# Modify this cell to insure that the output shows the correct path.\n", + "# Define all paths relative to the project root shown in the cell output\n", + "import os\n", + "from pathlib import Path\n", + "\n", + "project_root = \"somedir/freqtrade\"\n", + "i=0\n", + "try:\n", + " os.chdirdir(project_root)\n", + " assert Path('LICENSE').is_file()\n", + "except:\n", + " while i<4 and (not Path('LICENSE').is_file()):\n", + " os.chdir(Path(Path.cwd(), '../'))\n", + " i+=1\n", + " project_root = Path.cwd()\n", + "print(Path.cwd())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Watch project for changes to code\n", + "This scans the project for changes to code before Jupyter runs cells." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Reloads local code changes\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load existing objects into a Jupyter notebook\n", + "\n", + "These examples assume that you have already generated data using the cli. These examples will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load backtest results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load backtest results\n", + "from freqtrade.data.btanalysis import load_backtest_data\n", + "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", + "\n", + "# Show value-counts per pair\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load live trading results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch trades from database\n", + "from freqtrade.data.btanalysis import load_trades_from_db\n", + "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", + "\n", + "# Display results\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load multiple configuration files\n", + "This option can be useful to inspect the results of passing in multiple configs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load config from multiple files\n", + "from freqtrade.configuration import Configuration\n", + "config = Configuration.from_files([\"config1.json\", \"config2.json\"])\n", + "\n", + "# Show the config in memory\n", + "import json\n", + "print(json.dumps(config, indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load exchange data to a pandas dataframe\n", + "\n", + "This loads candle data to a dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Load data using values passed to function\n", + "from pathlib import Path\n", + "from freqtrade.data.history import load_pair_history\n", + "\n", + "ticker_interval = \"5m\"\n", + "data_location = Path('user_data', 'data', 'bitrex')\n", + "pair = \"BTC_USDT\"\n", + "candles = load_pair_history(datadir=data_location,\n", + " ticker_interval=ticker_interval,\n", + " pair=pair)\n", + "\n", + "# Confirm success\n", + "print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", + "display(candles.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data." + ] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + }, + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/strategy_analysis_example.ipynb similarity index 51% rename from user_data/notebooks/analysis_example.ipynb rename to user_data/notebooks/strategy_analysis_example.ipynb index f5e2c12d7..2b8b74fe6 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/strategy_analysis_example.ipynb @@ -1,96 +1,5 @@ { "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Analyzing bot data\n", - "\n", - "You can analyze the results of backtests and trading history easily using Jupyter notebooks. \n", - "**Copy this file so your changes don't get clobbered with the next freqtrade update!** \n", - "For usage instructions, see [jupyter.org](https://jupyter.org/documentation). \n", - "*Pro tip - Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Imports\n", - "from pathlib import Path\n", - "import os\n", - "from freqtrade.data.history import load_pair_history\n", - "from freqtrade.resolvers import StrategyResolver\n", - "from freqtrade.data.btanalysis import load_backtest_data\n", - "from freqtrade.data.btanalysis import load_trades_from_db" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Change directory\n", - "# Define all paths relative to the project root shown in the cell output\n", - "try:\n", - " os.chdir(Path(Path.cwd(), '../..'))\n", - " print(Path.cwd())\n", - "except:\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example snippets" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load backtest results into a pandas dataframe" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Load backtest results\n", - "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", - "\n", - "# Show value-counts per pair\n", - "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load live trading results into a pandas dataframe" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Fetch trades from database\n", - "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", - "\n", - "# Display results\n", - "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -104,7 +13,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Import requirements and define variables used in analyses" + "## Setup" ] }, { @@ -113,28 +22,51 @@ "metadata": {}, "outputs": [], "source": [ + "# Change directory\n", + "# Modify this cell to insure that the output shows the correct path.\n", + "# Define all paths relative to the project root shown in the cell output\n", + "import os\n", + "from pathlib import Path\n", + "\n", + "project_root = \"somedir/freqtrade\"\n", + "i=0\n", + "try:\n", + " os.chdirdir(project_root)\n", + " assert Path('LICENSE').is_file()\n", + "except:\n", + " while i<4 and (not Path('LICENSE').is_file()):\n", + " os.chdir(Path(Path.cwd(), '../'))\n", + " i+=1\n", + " project_root = Path.cwd()\n", + "print(Path.cwd())\n", + "\n", + "# Reloads local code changes\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Customize these according to your needs.\n", + "\n", "# Define some constants\n", "ticker_interval = \"5m\"\n", "# Name of the strategy class\n", - "strategy_name = 'AwesomeStrategy'\n", + "strategy_name = 'TestStrategy'\n", "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", "strategy_location = Path(user_data_dir, 'strategies')\n", "# Location of the data\n", "data_location = Path(user_data_dir, 'data', 'binance')\n", - "# Pair to analyze \n", - "# Only use one pair here\n", + "# Pair to analyze - Only use one pair here\n", "pair = \"BTC_USDT\"" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load exchange data" - ] - }, { "cell_type": "code", "execution_count": null, @@ -142,35 +74,43 @@ "outputs": [], "source": [ "# Load data using values set above\n", - "bt_data = load_pair_history(datadir=Path(data_location),\n", + "from pathlib import Path\n", + "from freqtrade.data.history import load_pair_history\n", + "\n", + "candles = load_pair_history(datadir=data_location,\n", " ticker_interval=ticker_interval,\n", " pair=pair)\n", "\n", "# Confirm success\n", - "print(\"Loaded \" + str(len(bt_data)) + f\" rows of data for {pair} from {data_location}\")" + "print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", + "display(candles.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Load and run strategy\n", + "## Load and run strategy\n", "* Rerun each time the strategy file is changed" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ "# Load strategy using values set above\n", + "from freqtrade.resolvers import StrategyResolver\n", "strategy = StrategyResolver({'strategy': strategy_name,\n", " 'user_data_dir': user_data_dir,\n", " 'strategy_path': strategy_location}).strategy\n", "\n", "# Generate buy/sell signals using strategy\n", - "df = strategy.analyze_ticker(bt_data, {'pair': pair})" + "df = strategy.analyze_ticker(candles, {'pair': pair})\n", + "df.tail()" ] }, { @@ -178,19 +118,14 @@ "metadata": {}, "source": [ "### Display the trade details\n", + "\n", "* Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n", - "\n", - "#### Some possible problems\n", - "\n", - "* Columns with NaN values at the end of the dataframe\n", - "* Columns used in `crossed*()` functions with completely different units\n", - "\n", - "#### Comparison with full backtest\n", - "\n", - "having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.\n", - "\n", - "Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple \"buy\" signals for each pair in sequence (until rsi returns > 29).\n", - "The bot will only buy on the first of these signals (and also only if a trade-slot (\"max_open_trades\") is still available), or on one of the middle signals, as soon as a \"slot\" becomes available.\n" + "* Some possible problems\n", + " * Columns with NaN values at the end of the dataframe\n", + " * Columns used in `crossed*()` functions with completely different units\n", + "* Comparison with full backtest\n", + " * having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.\n", + " * Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple \"buy\" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot (\"max_open_trades\") is still available), or on one of the middle signals, as soon as a \"slot\" becomes available. \n" ] }, { @@ -236,6 +171,48 @@ "name": "python", "npconvert_exporter": "python", "pygments_lexer": "ipython3", + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + }, "version": 3 }, "nbformat": 4, From 5e440a4cdc6baa4ecadcc37593f373fe06aeb257 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 06:55:19 +0200 Subject: [PATCH 225/269] Improve docs to point to `freqtrade download-data` --- docs/backtesting.md | 2 +- docs/bot-usage.md | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index f666c5b49..543422fee 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -3,7 +3,7 @@ This page explains how to validate your strategy performance by using Backtesting. -## Getting data for backtesting / hyperopt +## Getting data for backtesting and hyperopt To download backtesting data (candles / OHLCV), we recommend using the `freqtrade download-data` command. diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 31c5812ad..2873f5e8f 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -184,19 +184,11 @@ optional arguments: result.json) ``` -### How to use **--refresh-pairs-cached** parameter? +### Getting historic data for backtesting -The first time your run Backtesting, it will take the pairs you have -set in your config file and download data from the Exchange. - -If for any reason you want to update your data set, you use -`--refresh-pairs-cached` to force Backtesting to update the data it has. - -!!! Note - Use it only if you want to update your data set. You will not be able to come back to the previous version. - -To test your strategy with latest data, we recommend continuing using -the parameter `-l` or `--live`. +The first time your run Backtesting, you will need to download some historic data first. +This can be accomplished by using `freqtrade download-data`. +Check the corresponding [help page section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) for more details ## Hyperopt commands From 8a2a8ab8b5da8392857898684cf8d983e3cd1881 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 18 Aug 2019 12:47:19 +0300 Subject: [PATCH 226/269] docstring for ohlcv improved --- freqtrade/data/dataprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index e806f5aa7..b67aba045 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -45,7 +45,7 @@ class DataProvider(): def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: """ Get ohlcv data for the given pair as DataFrame - Please check `self.available_pairs` to verify which pairs are currently cached. + Please use the `available_pairs` method to verify which pairs are currently cached. :param pair: pair to get the data for :param ticker_interval: ticker interval to get data for :param copy: copy dataframe before returning if True. From 310e4387065bf1407c8226a0bec9f79dc62e98ec Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 18 Aug 2019 12:55:31 +0300 Subject: [PATCH 227/269] logging message improved --- freqtrade/data/dataprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b67aba045..b904ba985 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -85,7 +85,7 @@ class DataProvider(): # Get historic ohlcv data (cached on disk). data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval) if len(data) == 0: - logger.warning(f"No data found for pair {pair}") + logger.warning(f"No data found for ({pair}, {ticker_interval}).") return data def ticker(self, pair: str): From 407a3bca6222bd820d55656e73398dc4e6bef102 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 18 Aug 2019 13:00:37 +0300 Subject: [PATCH 228/269] implementation of ohlcv optimized --- freqtrade/data/dataprovider.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b904ba985..5b71c21a8 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -52,11 +52,10 @@ class DataProvider(): Use False only for read-only operations (where the dataframe is not modified) """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - pairtick = (pair, ticker_interval or self._config['ticker_interval']) - if pairtick in self.available_pairs: - return self._exchange.klines(pairtick, copy=copy) - - return DataFrame() + return self._exchange.klines((pair, ticker_interval or self._config['ticker_interval']), + copy=copy) + else: + return DataFrame() def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame: """ From d300964691977d34bc3618a8e75bb48b6ce863ae Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 18 Aug 2019 13:06:21 +0300 Subject: [PATCH 229/269] code formatting in test_dataprovider.py --- freqtrade/tests/data/test_dataprovider.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/data/test_dataprovider.py b/freqtrade/tests/data/test_dataprovider.py index 54aab7052..2272f69a3 100644 --- a/freqtrade/tests/data/test_dataprovider.py +++ b/freqtrade/tests/data/test_dataprovider.py @@ -13,6 +13,7 @@ def test_ohlcv(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history + dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) @@ -37,11 +38,9 @@ def test_ohlcv(mocker, default_conf, ticker_history): def test_historic_ohlcv(mocker, default_conf, ticker_history): - historymock = MagicMock(return_value=ticker_history) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) - # exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, None) data = dp.historic_ohlcv("UNITTEST/BTC", "5m") assert isinstance(data, DataFrame) @@ -57,6 +56,7 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history + dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) @@ -86,12 +86,11 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): def test_available_pairs(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) - ticker_interval = default_conf["ticker_interval"] exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf, exchange) assert len(dp.available_pairs) == 2 assert dp.available_pairs == [ ("XRP/BTC", ticker_interval), From acf1e734ecb71ad79a0bcc84dc55964d36530a4c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 15:09:44 +0200 Subject: [PATCH 230/269] Adapt lg_has calls to new standard --- freqtrade/tests/test_configuration.py | 2 +- freqtrade/tests/test_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 083be3e6e..9641d0a51 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -632,7 +632,7 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None: 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('Created user-data directory: /tmp/bar', caplog.record_tuples) + assert log_has('Created user-data directory: /tmp/bar', caplog) assert isinstance(x, Path) assert str(x) == "/tmp/bar" diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index 2bbece33f..8e364f3e5 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -53,7 +53,7 @@ def test_create_datadir_failed(caplog): ] with pytest.raises(SystemExit): start_create_userdir(get_args(args)) - assert log_has("`create-userdir` requires --userdir to be set.", caplog.record_tuples) + assert log_has("`create-userdir` requires --userdir to be set.", caplog) def test_create_datadir(caplog, mocker): From 8e96ac876597b686e66df3e4a238f324a9be95d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 15:45:30 +0200 Subject: [PATCH 231/269] Split exception tests for create_order --- freqtrade/tests/exchange/test_exchange.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 6b833054d..123f1fcfd 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -779,7 +779,13 @@ def test_sell_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200) + + # Market orders don't require price, so the behaviour is slightly different + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.sell(pair='ETH/BTC', ordertype='market', amount=1, rate=200) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) From ee7ba96e8524558d8d7ed068fb939d18aac0a711 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 15:46:10 +0200 Subject: [PATCH 232/269] Don't do calculations in exception handlers when one element can be None fixes #2011 --- freqtrade/exchange/exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5119e0fcd..67a79178f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -408,12 +408,12 @@ class Exchange(object): except ccxt.InsufficientFunds as e: raise DependencyException( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' + f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise DependencyException( f'Could not create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' + f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( From 045ac1019e37359bf6d04f9989af50493122b434 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 15:48:20 +0200 Subject: [PATCH 233/269] Split test for buy-orders too --- freqtrade/exchange/exchange.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 67a79178f..6e281b9b2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -472,7 +472,7 @@ class Exchange(object): order = self.create_order(pair, ordertype, 'sell', amount, rate, params) logger.info('stoploss limit order added for %s. ' - 'stop price: %s. limit: %s' % (pair, stop_price, rate)) + 'stop price: %s. limit: %s', pair, stop_price, rate) return order @retrier diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 123f1fcfd..397326258 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -656,7 +656,13 @@ def test_buy_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.buy(pair='ETH/BTC', ordertype=order_type, + exchange.buy(pair='ETH/BTC', ordertype='limit', + amount=1, rate=200, time_in_force=time_in_force) + + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.buy(pair='ETH/BTC', ordertype='market', amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(TemporaryError): From ddfadbb69ee2bf18191d94d053d3ef2301286030 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 16:10:10 +0200 Subject: [PATCH 234/269] Validate configuration consistency after loading strategy --- freqtrade/configuration/__init__.py | 1 + .../{json_schema.py => config_validation.py} | 35 ++++++++++++++++- freqtrade/configuration/configuration.py | 38 +++---------------- freqtrade/freqtradebot.py | 2 + freqtrade/tests/test_configuration.py | 14 +++---- 5 files changed, 47 insertions(+), 43 deletions(-) rename freqtrade/configuration/{json_schema.py => config_validation.py} (53%) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 7b476d173..ac59421a7 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,3 +1,4 @@ from freqtrade.configuration.arguments import Arguments # noqa: F401 from freqtrade.configuration.timerange import TimeRange # noqa: F401 from freqtrade.configuration.configuration import Configuration # noqa: F401 +from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401 diff --git a/freqtrade/configuration/json_schema.py b/freqtrade/configuration/config_validation.py similarity index 53% rename from freqtrade/configuration/json_schema.py rename to freqtrade/configuration/config_validation.py index 4c6f4a4a0..bda60f90b 100644 --- a/freqtrade/configuration/json_schema.py +++ b/freqtrade/configuration/config_validation.py @@ -4,7 +4,7 @@ from typing import Any, Dict from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match -from freqtrade import constants +from freqtrade import constants, OperationalException logger = logging.getLogger(__name__) @@ -51,3 +51,36 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: raise ValidationError( best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message ) + + +def validate_config_consistency(conf: Dict[str, Any]) -> None: + """ + Validate the configuration consistency. + Should be ran after loading both configuration and strategy, + since strategies can set certain configuration settings too. + :param conf: Config in JSON format + :return: Returns None if everything is ok, otherwise throw an OperationalException + """ + # validating trailing stoploss + _validate_trailing_stoploss(conf) + + +def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: + + # Skip if trailing stoploss is not activated + if not conf.get('trailing_stop', False): + return + + tsl_positive = float(conf.get('trailing_stop_positive', 0)) + tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) + tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) + + if tsl_only_offset: + if tsl_positive == 0.0: + raise OperationalException( + f'The config trailing_only_offset_is_reached needs ' + 'trailing_stop_positive_offset to be more than 0 in your config.') + if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: + raise OperationalException( + f'The config trailing_stop_positive_offset needs ' + 'to be greater than trailing_stop_positive_offset in your config.') diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index c95246fc0..486857153 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -6,10 +6,11 @@ import warnings from argparse import Namespace from typing import Any, Callable, Dict, List, Optional -from freqtrade import OperationalException, constants +from freqtrade import constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.create_datadir import create_datadir -from freqtrade.configuration.json_schema import validate_config_schema +from freqtrade.configuration.config_validation import (validate_config_schema, + validate_config_consistency) from freqtrade.configuration.load_config import load_config_file from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts @@ -77,8 +78,6 @@ class Configuration(object): # Load all configs config: Dict[str, Any] = Configuration.from_files(self.args.config) - self._validate_config_consistency(config) - self._process_common_options(config) self._process_optimize_options(config) @@ -87,6 +86,8 @@ class Configuration(object): self._process_runmode(config) + validate_config_consistency(config) + return config def _process_logging_options(self, config: Dict[str, Any]) -> None: @@ -285,35 +286,6 @@ class Configuration(object): config.update({'runmode': self.runmode}) - def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: - """ - Validate the configuration consistency - :param conf: Config in JSON format - :return: Returns None if everything is ok, otherwise throw an OperationalException - """ - # validating trailing stoploss - self._validate_trailing_stoploss(conf) - - def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None: - - # Skip if trailing stoploss is not activated - if not conf.get('trailing_stop', False): - return - - tsl_positive = float(conf.get('trailing_stop_positive', 0)) - tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) - tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) - - if tsl_only_offset: - if tsl_positive == 0.0: - raise OperationalException( - f'The config trailing_only_offset_is_reached needs ' - 'trailing_stop_positive_offset to be more than 0 in your config.') - if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: - raise OperationalException( - f'The config trailing_stop_positive_offset needs ' - 'to be greater than trailing_stop_positive_offset in your config.') - def _args_to_config(self, config: Dict[str, Any], argname: str, logstring: str, logfun: Optional[Callable] = None, deprecated_msg: Optional[str] = None) -> None: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 68b45d96f..af29604f5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,6 +16,7 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge +from freqtrade.configuration import validate_config_consistency from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType @@ -50,6 +51,7 @@ class FreqtradeBot(object): self.config = config self.strategy: IStrategy = StrategyResolver(self.config).strategy + validate_config_consistency(config) self.rpc: RPCManager = RPCManager(self) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 8cbd02ece..3a0077ef2 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -2,7 +2,6 @@ import json import logging import warnings -from argparse import Namespace from copy import deepcopy from pathlib import Path from unittest.mock import MagicMock @@ -11,10 +10,10 @@ import pytest from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants -from freqtrade.configuration import Arguments, Configuration +from freqtrade.configuration import Arguments, Configuration, validate_config_consistency from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.create_datadir import create_datadir -from freqtrade.configuration.json_schema import validate_config_schema 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 @@ -625,21 +624,18 @@ def test_validate_tsl(default_conf): with pytest.raises(OperationalException, match=r'The config trailing_only_offset_is_reached needs ' 'trailing_stop_positive_offset to be more than 0 in your config.'): - configuration = Configuration(Namespace()) - configuration._validate_config_consistency(default_conf) + validate_config_consistency(default_conf) default_conf['trailing_stop_positive_offset'] = 0.01 default_conf['trailing_stop_positive'] = 0.015 with pytest.raises(OperationalException, match=r'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive_offset in your config.'): - configuration = Configuration(Namespace()) - configuration._validate_config_consistency(default_conf) + validate_config_consistency(default_conf) default_conf['trailing_stop_positive'] = 0.01 default_conf['trailing_stop_positive_offset'] = 0.015 - Configuration(Namespace()) - configuration._validate_config_consistency(default_conf) + validate_config_consistency(default_conf) def test_load_config_test_comments() -> None: From 611850bf91a343d056259f3bb6ab7bc9b683c59f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 16:19:24 +0200 Subject: [PATCH 235/269] Add edge/dynamic_whitelist validation --- freqtrade/configuration/config_validation.py | 16 ++++++++++++++++ freqtrade/tests/test_configuration.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index bda60f90b..92846b704 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -63,6 +63,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: """ # validating trailing stoploss _validate_trailing_stoploss(conf) + _validate_edge(conf) def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: @@ -84,3 +85,18 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: raise OperationalException( f'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive_offset in your config.') + + +def _validate_edge(conf: Dict[str, Any]) -> None: + """ + Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists. + """ + + if not conf.get('edge', {}).get('enabled'): + return + + if conf.get('pairlist', {}).get('method') == 'VolumePairList': + raise OperationalException( + "Edge and VolumePairList are incompatible, " + "Edge will override whatever pairs VolumePairlist selects." + ) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 3a0077ef2..19d2a28ee 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -638,6 +638,22 @@ def test_validate_tsl(default_conf): validate_config_consistency(default_conf) +def test_validate_edge(edge_conf): + edge_conf.update({"pairlist": { + "method": "VolumePairList", + }}) + + with pytest.raises(OperationalException, + match="Edge and VolumePairList are incompatible, " + "Edge will override whatever pairs VolumePairlist selects."): + validate_config_consistency(edge_conf) + + edge_conf.update({"pairlist": { + "method": "StaticPairList", + }}) + validate_config_consistency(edge_conf) + + def test_load_config_test_comments() -> None: """ Load config with comments From b6462cd51f707f4160fa77d8bf2eb36c73a7695e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 16:22:18 +0200 Subject: [PATCH 236/269] Add explaining comment --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index af29604f5..e5ecef8bf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -51,6 +51,8 @@ class FreqtradeBot(object): self.config = config self.strategy: IStrategy = StrategyResolver(self.config).strategy + + # Check config consistency here since strategies can set certain options validate_config_consistency(config) self.rpc: RPCManager = RPCManager(self) From d785d7637085edcaf382a26df2d71cb8612f6a7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 18:06:36 +0200 Subject: [PATCH 237/269] make VolumePairlist less verbose no need to print the full whitelist on every iteration --- freqtrade/constants.py | 1 - freqtrade/pairlist/VolumePairList.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9b73adcfe..bd3ba21fd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -5,7 +5,6 @@ bot constants """ DEFAULT_CONFIG = 'config.json' DEFAULT_EXCHANGE = 'bittrex' -DYNAMIC_WHITELIST = 20 # pairs PROCESS_THROTTLE_SECS = 5 # sec DEFAULT_TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 9a2e2eac4..b9b7977ab 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -55,7 +55,6 @@ class VolumePairList(IPairList): # Generate dynamic whitelist self._whitelist = self._gen_pair_whitelist( self._config['stake_currency'], self._sort_key)[:self._number_pairs] - logger.info(f"Searching pairs: {self._whitelist}") @cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: @@ -92,4 +91,6 @@ class VolumePairList(IPairList): valid_tickers.remove(t) pairs = [s['symbol'] for s in valid_tickers] + logger.info(f"Searching pairs: {self._whitelist}") + return pairs From ea4db0ffb6dd0007b4df471a3e3f8f90d9e3599d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 18:06:57 +0200 Subject: [PATCH 238/269] Pass object-name to loader to fix logging --- freqtrade/resolvers/iresolver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 841c3cf43..310c54015 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -29,7 +29,8 @@ class IResolver(object): """ # Generate spec based on absolute path - spec = importlib.util.spec_from_file_location('unknown', str(module_path)) + # Pass object_name as first argument to have logging print a reasonable name. + spec = importlib.util.spec_from_file_location(object_name, str(module_path)) module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints From a4ede02cedd22ddc1147f1fcffd224b554af970d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 19:38:23 +0200 Subject: [PATCH 239/269] Gracefully handle problems with dry-run orders --- freqtrade/exchange/exchange.py | 9 +++++++-- freqtrade/tests/exchange/test_exchange.py | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5119e0fcd..7ae40381d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -696,8 +696,13 @@ class Exchange(object): @retrier def get_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - order = self._dry_run_open_orders[order_id] - return order + try: + order = self._dry_run_open_orders[order_id] + return order + except KeyError as e: + # Gracefully handle errors with dry-run orders. + raise InvalidOrderException( + f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e try: return self._api.fetch_order(order_id, pair) except ccxt.InvalidOrder as e: diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 6b833054d..6a7dfa04b 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1328,6 +1328,9 @@ def test_get_order(default_conf, mocker, exchange_name): print(exchange.get_order('X', 'TKN/BTC')) assert exchange.get_order('X', 'TKN/BTC').myid == 123 + with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): + exchange.get_order('Y', 'TKN/BTC') + default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) From 9ad9ce0da1b46a0822cba4f100257a177211530f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2019 10:52:53 +0000 Subject: [PATCH 240/269] Bump ccxt from 1.18.1063 to 1.18.1068 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1063 to 1.18.1068. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.18.1063...1.18.1068) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 4666fc053..3d80c3ef5 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.18.1063 +ccxt==1.18.1068 SQLAlchemy==1.3.7 python-telegram-bot==11.1.0 arrow==0.14.5 From 70b1a05d976aad8a7c808ba7ee8a27156081b7b8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 20 Aug 2019 01:32:02 +0300 Subject: [PATCH 241/269] example in the docs changed --- docs/strategy-customization.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0d08bdd02..d71ebfded 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -274,27 +274,24 @@ Please always check the mode of operation to select the correct method to get da #### Possible options for DataProvider -- `available_pairs` - Property with tuples listing cached pairs with their intervals. (pair, interval) -- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for all pairs in the whitelist, returns DataFrame or empty DataFrame -- `historic_ohlcv(pair, ticker_interval)` - Data stored on disk +- `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval). +- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame. +- `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk. +- `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - `runmode` - Property containing the current runmode. -#### ohlcv / historic_ohlcv +#### Example: fetch live ohlcv / historic data for the first informative pair ``` python if self.dp: - if self.dp.runmode in ('live', 'dry_run'): - if (f'{self.stake_currency}/BTC', self.ticker_interval) in self.dp.available_pairs: - data_eth = self.dp.ohlcv(pair='{self.stake_currency}/BTC', - ticker_interval=self.ticker_interval) - else: - # Get historic ohlcv data (cached on disk). - history_eth = self.dp.historic_ohlcv(pair='{self.stake_currency}/BTC', - ticker_interval='1h') + inf_pair, inf_timeframe = self.informative_pairs()[0] + informative = self.dp.get_pair_dataframe(pair=inf_pair, + ticker_interval=inf_timeframe) ``` !!! Warning Warning about backtesting - Be carefull when using dataprovider in backtesting. `historic_ohlcv()` provides the full time-range in one go, + Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` + for the backtesting runmode) provides the full time-range in one go, so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). !!! Warning Warning in hyperopt From 8d1a575a9b975e1753d312fbf72d8e4f640354f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Aug 2019 06:39:28 +0200 Subject: [PATCH 242/269] Reword documentation --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 543422fee..3712fddce 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -5,7 +5,7 @@ Backtesting. ## Getting data for backtesting and hyperopt -To download backtesting data (candles / OHLCV), we recommend using the `freqtrade download-data` command. +To download backtesting data (candles / OHLCV) and hyperoptimization using the `freqtrade download-data` command. If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes. Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. From 8cc477f3533aeebe1ded157298a415d2fbbcc719 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Tue, 20 Aug 2019 00:47:10 -0400 Subject: [PATCH 243/269] edits --- docs/data-analysis.md | 65 ++-- .../notebooks/jupyter_snippets_example.ipynb | 289 ------------------ .../notebooks/strategy_analysis_example.ipynb | 42 ++- 3 files changed, 60 insertions(+), 336 deletions(-) delete mode 100644 user_data/notebooks/jupyter_snippets_example.ipynb diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 8d4cc63c0..4387dd892 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -5,19 +5,19 @@ You can analyze the results of backtests and trading history easily using Jupyte ## Pro tips * See [jupyter.org](https://jupyter.org/documentation) for usage instructions. -* Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* -* Copy the example notebook so your changes don't get clobbered with the next freqtrade update. +* Don't forget to start a Jupyter notebook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* +* Copy the example notebook before use so your changes don't get clobbered with the next freqtrade update. ## Fine print -Some tasks don't work especially well in notebooks. For example, anything using asyncronous exectution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required parameters to functions. +Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually. ## Recommended workflow | Task | Tool | --- | --- Bot operations | CLI -Repetative tasks | shell scripts +Repetitive tasks | Shell scripts Data analysis & visualization | Notebook 1. Use the CLI to @@ -28,26 +28,26 @@ Data analysis & visualization | Notebook 1. Collect these actions in shell scripts * save complicated commands with arguments - * execute mult-step operations - * automate testing strategies and prepareing data for analysis + * execute multi-step operations + * automate testing strategies and preparing data for analysis 1. Use a notebook to - * import data + * visualize data * munge and plot to generate insights -## Example utility snippets for Jupyter notebooks +## Example utility snippets ### Change directory to root -Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistant. +Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistent. ```python -# Change directory -# Modify this cell to insure that the output shows the correct path. -# Define all paths relative to the project root shown in the cell output import os from pathlib import Path +# Change directory +# Modify this cell to insure that the output shows the correct path. +# Define all paths relative to the project root shown in the cell output project_root = "somedir/freqtrade" i=0 try: @@ -61,25 +61,16 @@ except: print(Path.cwd()) ``` -### Watch project for changes to code - -This scans the project for changes to code before Jupyter runs cells. - -```python -# Reloads local code changes -%load_ext autoreload -%autoreload 2 -``` - ## Load existing objects into a Jupyter notebook -These examples assume that you have already generated data using the cli. These examples will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload. +These examples assume that you have already generated data using the cli. They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload. ### Load backtest results into a pandas dataframe ```python -# Load backtest results from freqtrade.data.btanalysis import load_backtest_data + +# Load backtest results df = load_backtest_data("user_data/backtest_data/backtest-result.json") # Show value-counts per pair @@ -89,8 +80,9 @@ df.groupby("pair")["sell_reason"].value_counts() ### Load live trading results into a pandas dataframe ``` python -# Fetch trades from database from freqtrade.data.btanalysis import load_trades_from_db + +# Fetch trades from database df = load_trades_from_db("sqlite:///tradesv3.sqlite") # Display results @@ -102,12 +94,13 @@ df.groupby("pair")["sell_reason"].value_counts() This option can be useful to inspect the results of passing in multiple configs ``` python -# Load config from multiple files +import json from freqtrade.configuration import Configuration + +# Load config from multiple files config = Configuration.from_files(["config1.json", "config2.json"]) # Show the config in memory -import json print(json.dumps(config, indent=1)) ``` @@ -116,10 +109,10 @@ print(json.dumps(config, indent=1)) This loads candle data to a dataframe ```python -# Load data using values passed to function from pathlib import Path from freqtrade.data.history import load_pair_history +# Load data using values passed to function ticker_interval = "5m" data_location = Path('user_data', 'data', 'bitrex') pair = "BTC_USDT" @@ -128,8 +121,8 @@ candles = load_pair_history(datadir=data_location, pair=pair) # Confirm success -print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}") -display(candles.head()) +print(f"Loaded len(candles) rows of data for {pair} from {data_location}") +candles.head() ``` ## Strategy debugging example @@ -160,17 +153,17 @@ pair = "BTC_USDT" ### Load exchange data ```python -# Load data using values set above from pathlib import Path from freqtrade.data.history import load_pair_history +# Load data using values set above candles = load_pair_history(datadir=data_location, ticker_interval=ticker_interval, pair=pair) # Confirm success -print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}") -display(candles.head()) +print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}") +candles.head() ``` ### Load and run strategy @@ -178,8 +171,9 @@ display(candles.head()) * Rerun each time the strategy file is changed ```python -# Load strategy using values set above from freqtrade.resolvers import StrategyResolver + +# Load strategy using values set above strategy = StrategyResolver({'strategy': strategy_name, 'user_data_dir': user_data_dir, 'strategy_path': strategy_location}).strategy @@ -190,7 +184,7 @@ df = strategy.analyze_ticker(candles, {'pair': pair}) ### Display the trade details -* Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. +* Note that using `data.tail()` is preferable to `data.head()` as most indicators have some "startup" data at the top of the dataframe. * Some possible problems * Columns with NaN values at the end of the dataframe * Columns used in `crossed*()` functions with completely different units @@ -199,7 +193,6 @@ df = strategy.analyze_ticker(candles, {'pair': pair}) * Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available. ```python - # Report results print(f"Generated {df['buy'].sum()} buy signals") data = df.set_index('date', drop=True) diff --git a/user_data/notebooks/jupyter_snippets_example.ipynb b/user_data/notebooks/jupyter_snippets_example.ipynb deleted file mode 100644 index 9d4ae467a..000000000 --- a/user_data/notebooks/jupyter_snippets_example.ipynb +++ /dev/null @@ -1,289 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Analyzing bot data with Jupyter notebooks \n", - "\n", - "You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. \n", - "\n", - "## Pro tips \n", - "\n", - "* See [jupyter.org](https://jupyter.org/documentation) for usage instructions.\n", - "* Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*\n", - "* Copy the example notebook so your changes don't get clobbered with the next freqtrade update.\n", - "\n", - "## Fine print \n", - "\n", - "Some tasks don't work especially well in notebooks. For example, anything using asyncronous exectution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required parameters to functions.\n", - "\n", - "## Recommended workflow \n", - "\n", - "| Task | Tool | \n", - " --- | --- \n", - "Bot operations | CLI \n", - "Repetative tasks | shell scripts\n", - "Data analysis & visualization | Notebook \n", - "\n", - "1. Use the CLI to\n", - " * download historical data\n", - " * run a backtest\n", - " * run with real-time data\n", - " * export results \n", - "\n", - "1. Collect these actions in shell scripts\n", - " * save complicated commands with arguments\n", - " * execute mult-step operations \n", - " * automate testing strategies and prepareing data for analysis\n", - "\n", - "1. Use a notebook to\n", - " * import data\n", - " * munge and plot to generate insights" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example utility snippets for Jupyter notebooks" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Change directory to root \n", - "\n", - "Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistant." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Change directory\n", - "# Modify this cell to insure that the output shows the correct path.\n", - "# Define all paths relative to the project root shown in the cell output\n", - "import os\n", - "from pathlib import Path\n", - "\n", - "project_root = \"somedir/freqtrade\"\n", - "i=0\n", - "try:\n", - " os.chdirdir(project_root)\n", - " assert Path('LICENSE').is_file()\n", - "except:\n", - " while i<4 and (not Path('LICENSE').is_file()):\n", - " os.chdir(Path(Path.cwd(), '../'))\n", - " i+=1\n", - " project_root = Path.cwd()\n", - "print(Path.cwd())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Watch project for changes to code\n", - "This scans the project for changes to code before Jupyter runs cells." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Reloads local code changes\n", - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Load existing objects into a Jupyter notebook\n", - "\n", - "These examples assume that you have already generated data using the cli. These examples will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load backtest results into a pandas dataframe" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Load backtest results\n", - "from freqtrade.data.btanalysis import load_backtest_data\n", - "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", - "\n", - "# Show value-counts per pair\n", - "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load live trading results into a pandas dataframe" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Fetch trades from database\n", - "from freqtrade.data.btanalysis import load_trades_from_db\n", - "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", - "\n", - "# Display results\n", - "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load multiple configuration files\n", - "This option can be useful to inspect the results of passing in multiple configs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Load config from multiple files\n", - "from freqtrade.configuration import Configuration\n", - "config = Configuration.from_files([\"config1.json\", \"config2.json\"])\n", - "\n", - "# Show the config in memory\n", - "import json\n", - "print(json.dumps(config, indent=1))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load exchange data to a pandas dataframe\n", - "\n", - "This loads candle data to a dataframe" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "# Load data using values passed to function\n", - "from pathlib import Path\n", - "from freqtrade.data.history import load_pair_history\n", - "\n", - "ticker_interval = \"5m\"\n", - "data_location = Path('user_data', 'data', 'bitrex')\n", - "pair = \"BTC_USDT\"\n", - "candles = load_pair_history(datadir=data_location,\n", - " ticker_interval=ticker_interval,\n", - " pair=pair)\n", - "\n", - "# Confirm success\n", - "print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", - "display(candles.head())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data." - ] - } - ], - "metadata": { - "file_extension": ".py", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "mimetype": "text/x-python", - "name": "python", - "npconvert_exporter": "python", - "pygments_lexer": "ipython3", - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - }, - "version": 3 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/user_data/notebooks/strategy_analysis_example.ipynb index 2b8b74fe6..bb35d3e91 100644 --- a/user_data/notebooks/strategy_analysis_example.ipynb +++ b/user_data/notebooks/strategy_analysis_example.ipynb @@ -24,10 +24,10 @@ "source": [ "# Change directory\n", "# Modify this cell to insure that the output shows the correct path.\n", - "# Define all paths relative to the project root shown in the cell output\n", "import os\n", "from pathlib import Path\n", "\n", + "# Define all paths relative to the project root shown in the cell output\n", "project_root = \"somedir/freqtrade\"\n", "i=0\n", "try:\n", @@ -38,11 +38,7 @@ " os.chdir(Path(Path.cwd(), '../'))\n", " i+=1\n", " project_root = Path.cwd()\n", - "print(Path.cwd())\n", - "\n", - "# Reloads local code changes\n", - "%load_ext autoreload\n", - "%autoreload 2" + "print(Path.cwd())" ] }, { @@ -69,9 +65,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'freqtrade'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# Load data using values set above\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mpathlib\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mPath\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32mfrom\u001b[0m \u001b[0mfreqtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhistory\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mload_pair_history\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m candles = load_pair_history(datadir=data_location,\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'freqtrade'" + ] + } + ], "source": [ "# Load data using values set above\n", "from pathlib import Path\n", @@ -83,7 +91,7 @@ "\n", "# Confirm success\n", "print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", - "display(candles.head())" + "candles.head()" ] }, { @@ -96,11 +104,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'freqtrade'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# Load strategy using values set above\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32mfrom\u001b[0m \u001b[0mfreqtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresolvers\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mStrategyResolver\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m strategy = StrategyResolver({'strategy': strategy_name,\n\u001b[1;32m 4\u001b[0m \u001b[0;34m'user_data_dir'\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0muser_data_dir\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m 'strategy_path': strategy_location}).strategy\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'freqtrade'" + ] + } + ], "source": [ "# Load strategy using values set above\n", "from freqtrade.resolvers import StrategyResolver\n", From e9e2a8343692e80307d555d8fe19227556f276f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 06:59:56 +0200 Subject: [PATCH 244/269] remove `--live` references --- docs/backtesting.md | 14 +------------ docs/bot-usage.md | 1 - docs/plotting.md | 8 +------ freqtrade/configuration/arguments.py | 4 ++-- freqtrade/configuration/cli_options.py | 5 ----- freqtrade/configuration/configuration.py | 4 ---- freqtrade/tests/optimize/test_backtesting.py | 22 +++----------------- freqtrade/tests/optimize/test_hyperopt.py | 3 --- freqtrade/tests/test_arguments.py | 2 -- freqtrade/tests/test_configuration.py | 8 ------- freqtrade/tests/test_main.py | 1 - 11 files changed, 7 insertions(+), 65 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 3712fddce..5e7cb93dc 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -67,22 +67,10 @@ freqtrade backtesting freqtrade backtesting --ticker-interval 1m ``` -#### Update cached pairs with the latest data - -```bash -freqtrade backtesting --refresh-pairs-cached -``` - -#### With live data (do not alter your testdata files) - -```bash -freqtrade backtesting --live -``` - #### Using a different on-disk ticker-data source ```bash -freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 +freqtrade backtesting --datadir user_data/data/bittrex-20180101 ``` #### With a (custom) strategy file diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 2873f5e8f..b8068d96d 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -166,7 +166,6 @@ optional arguments: Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number). - -l, --live Use live data. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to backtest Please note that ticker-interval needs to be diff --git a/docs/plotting.md b/docs/plotting.md index b8e041d61..ac0b1f7cf 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -15,7 +15,7 @@ pip install -U -r requirements-plot.txt Usage for the price plotter: ``` bash -python3 script/plot_dataframe.py [-h] [-p pairs] [--live] +python3 script/plot_dataframe.py [-h] [-p pairs] ``` Example @@ -41,12 +41,6 @@ To plot multiple pairs, separate them with a comma: python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH ``` -To plot the current live price use the `--live` flag: - -``` bash -python3 scripts/plot_dataframe.py -p BTC/ETH --live -``` - To plot a timerange (to zoom in): ``` bash diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index c45e3d7ba..f7a63adc8 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -17,7 +17,7 @@ ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount", "refresh_pairs"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", - "live", "strategy_list", "export", "exportfilename"] + "strategy_list", "export", "exportfilename"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "position_stacking", "epochs", "spaces", @@ -35,7 +35,7 @@ ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", " ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", "timerange", - "refresh_pairs", "live"]) + "refresh_pairs"]) ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index d39013737..1240f6ec5 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -123,11 +123,6 @@ AVAILABLE_CLI_OPTIONS = { action='store_false', default=True, ), - "live": Arg( - '-l', '--live', - help='Use live data.', - action='store_true', - ), "strategy_list": Arg( '--strategy-list', help='Provide a space-separated list of strategies to backtest. ' diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 329058cef..784600dfb 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -175,10 +175,6 @@ class Configuration(object): logstring='Parameter -i/--ticker-interval detected ... ' 'Using ticker_interval: {} ...') - self._args_to_config(config, argname='live', - logstring='Parameter -l/--live detected ...', - deprecated_msg='--live will be removed soon.') - self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 02e9a9c28..a39f57eaf 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -186,9 +186,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'ticker_interval' in config assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog) - assert 'live' not in config - assert not log_has('Parameter -l/--live detected ...', caplog) - assert 'position_stacking' not in config assert not log_has('Parameter --enable-position-stacking detected ...', caplog) @@ -201,7 +198,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST -@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( @@ -215,7 +211,6 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> '--datadir', '/foo/bar', 'backtesting', '--ticker-interval', '1m', - '--live', '--enable-position-stacking', '--disable-max-market-positions', '--refresh-pairs-cached', @@ -238,9 +233,6 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', caplog) - assert 'live' in config - assert log_has('Parameter -l/--live detected ...', caplog) - assert 'position_stacking' in config assert log_has('Parameter --enable-position-stacking detected ...', caplog) @@ -815,8 +807,7 @@ def test_backtest_record(default_conf, fee, mocker): assert dur > 0 -@pytest.mark.filterwarnings("ignore:DEPRECATED") -def test_backtest_start_live(default_conf, mocker, caplog): +def test_backtest_start_timerange(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] async def load_pairs(pair, timeframe, since): @@ -836,7 +827,6 @@ def test_backtest_start_live(default_conf, mocker, caplog): '--datadir', 'freqtrade/tests/testdata', 'backtesting', '--ticker-interval', '1m', - '--live', '--timerange', '-100', '--enable-position-stacking', '--disable-max-market-positions' @@ -846,14 +836,12 @@ def test_backtest_start_live(default_conf, mocker, caplog): # check the logs, that will contain the backtest result exists = [ 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', - 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', 'Using data directory: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Live: Downloading data for all defined pairs ...', - 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' + 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...' ] @@ -862,7 +850,6 @@ def test_backtest_start_live(default_conf, mocker, caplog): assert log_has(line, caplog) -@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_multi_strat(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] @@ -886,7 +873,6 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): '--datadir', 'freqtrade/tests/testdata', 'backtesting', '--ticker-interval', '1m', - '--live', '--timerange', '-100', '--enable-position-stacking', '--disable-max-market-positions', @@ -904,14 +890,12 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): # check the logs, that will contain the backtest result exists = [ 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', - 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', 'Using data directory: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Live: Downloading data for all defined pairs ...', - 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' + 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 1c4e2445c..dd39981e5 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -83,9 +83,6 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca assert 'ticker_interval' in config assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog) - assert 'live' not in config - assert not log_has('Parameter -l/--live detected ...', caplog) - assert 'position_stacking' not in config assert not log_has('Parameter --enable-position-stacking detected ...', caplog) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 601f41e63..24f11e32e 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -98,7 +98,6 @@ def test_parse_args_backtesting_custom() -> None: args = [ '-c', 'test_conf.json', 'backtesting', - '--live', '--ticker-interval', '1m', '--refresh-pairs-cached', '--strategy-list', @@ -107,7 +106,6 @@ def test_parse_args_backtesting_custom() -> None: ] call_args = Arguments(args, '').get_parsed_arg() assert call_args.config == ['test_conf.json'] - assert call_args.live is True assert call_args.verbosity == 0 assert call_args.subparser == 'backtesting' assert call_args.func is not None diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 5cfee0698..6a3655f88 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -335,9 +335,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'ticker_interval' in config assert not log_has('Parameter -i/--ticker-interval detected ...', caplog) - assert 'live' not in config - assert not log_has('Parameter -l/--live detected ...', caplog) - assert 'position_stacking' not in config assert not log_has('Parameter --enable-position-stacking detected ...', caplog) @@ -348,7 +345,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'export' not in config -@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( @@ -362,7 +358,6 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non '--datadir', '/foo/bar', 'backtesting', '--ticker-interval', '1m', - '--live', '--enable-position-stacking', '--disable-max-market-positions', '--refresh-pairs-cached', @@ -385,9 +380,6 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', caplog) - assert 'live' in config - assert log_has('Parameter -l/--live detected ...', caplog) - assert 'position_stacking'in config assert log_has('Parameter --enable-position-stacking detected ...', caplog) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 409025a3c..db5a438d0 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -28,7 +28,6 @@ def test_parse_args_backtesting(mocker) -> None: assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] assert call_args.config == ['config.json'] - assert call_args.live is False assert call_args.verbosity == 0 assert call_args.subparser == 'backtesting' assert call_args.func is not None From 9e249928357544e49a2548f4d463f37764ae0ee4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 07:09:02 +0200 Subject: [PATCH 245/269] Remove calls to load_data using live= --- freqtrade/optimize/backtesting.py | 1 - freqtrade/plot/plotting.py | 3 +-- freqtrade/tests/optimize/test_backtesting.py | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3878d7603..568615b53 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -413,7 +413,6 @@ class Backtesting(object): refresh_pairs=self.config.get('refresh_pairs', False), exchange=self.exchange, timerange=timerange, - live=self.config.get('live', False) ) if not data: diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e6da581a4..2f09bd986 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -31,7 +31,7 @@ def init_plotscript(config): exchange: Optional[Exchange] = None # Exchange is only needed when downloading data! - if config.get("live", False) or config.get("refresh_pairs", False): + if config.get("refresh_pairs", False): exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange @@ -51,7 +51,6 @@ def init_plotscript(config): refresh_pairs=config.get('refresh_pairs', False), timerange=timerange, exchange=exchange, - live=config.get("live", False), ) trades = load_trades(config) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index a39f57eaf..c7dead1eb 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -471,7 +471,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = '1m' - default_conf['live'] = False default_conf['datadir'] = None default_conf['export'] = None default_conf['timerange'] = '-100' @@ -505,7 +504,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = "1m" - default_conf['live'] = False default_conf['datadir'] = None default_conf['export'] = None default_conf['timerange'] = '20180101-20180102' From f02adf2a45f5808a0f192f77c5d66841431d8d9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 07:18:21 +0200 Subject: [PATCH 246/269] Deprecate --refresh-pairs-cached --- freqtrade/configuration/configuration.py | 3 ++- freqtrade/tests/optimize/test_backtesting.py | 1 + freqtrade/tests/optimize/test_edge_cli.py | 6 +++++- freqtrade/tests/optimize/test_hyperopt.py | 1 + freqtrade/tests/test_configuration.py | 1 + 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 784600dfb..95110a280 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -199,7 +199,8 @@ class Configuration(object): self._process_datadir_options(config) self._args_to_config(config, argname='refresh_pairs', - logstring='Parameter -r/--refresh-pairs-cached detected ...') + logstring='Parameter -r/--refresh-pairs-cached detected ...', + deprecated_msg='-r/--refresh-pairs-cached will be removed soon.') self._args_to_config(config, argname='strategy_list', logstring='Using strategy list of {} Strategies', logfun=len) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index c7dead1eb..5c942ab72 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -198,6 +198,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index cdc724db2..25ad48e43 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -3,11 +3,14 @@ from unittest.mock import MagicMock +import pytest + from freqtrade.edge import PairInfo from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode -from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) @@ -40,6 +43,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'stoploss_range' not in config +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: patched_configuration_load_config_file(mocker, edge_conf) mocker.patch( diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index dd39981e5..d233f96b8 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -94,6 +94,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca assert config['runmode'] == RunMode.HYPEROPT +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 6a3655f88..2ae936416 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -345,6 +345,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'export' not in config +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( From 11dab2b9ca0f12cdaf45bf70428771cb99748c51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 07:22:03 +0200 Subject: [PATCH 247/269] Deprecate documentation for --refresh-pairs-cached --- docs/deprecated.md | 9 +++++---- docs/edge.md | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/deprecated.md b/docs/deprecated.md index 2bf655191..9fd27c3f9 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,12 +4,13 @@ This page contains description of the command line arguments, configuration para and the bot features that were declared as DEPRECATED by the bot development team and are no longer supported. Please avoid their usage in your configuration. -### the `--live` command line option +### the `--refresh-pairs-cached` command line option -`--live` in the context of backtesting allows to download the latest tick data for backtesting. -Since this only downloads one set of data (by default 500 candles) - this is not really suitable for extendet backtesting, and has therefore been deprecated. +`--refresh-pairs-cached` in the context of backtesting, hyperopt and edge allowes to refresh candle data for backtesting. +Since this leads to much confusion, and slows down backtesting (while not beeing part of backtesting) this has been singled out +as a seperate freqtrade subcommand `freqtrade download-data`. -This command was deprecated in `2019.6-dev` and will be removed after the next release. +This command was deprecated in `2019.7-dev` and will be removed after the next release. ## Removed features diff --git a/docs/edge.md b/docs/edge.md index 9047758f4..60cdfd830 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -234,9 +234,8 @@ An example of its output: ### Update cached pairs with the latest data -```bash -freqtrade edge --refresh-pairs-cached -``` +Edge requires historic data the same way than backtesting does. +Please refer to the [download section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) of the documentation for details. ### Precising stoploss range From 4ee35438a78fe0543609b8fb8e67cafb39509838 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Aug 2019 07:05:17 +0200 Subject: [PATCH 248/269] Improve deprecated docs --- docs/backtesting.md | 3 +++ docs/deprecated.md | 12 +++++++++--- docs/edge.md | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 5e7cb93dc..f74d07df1 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -69,6 +69,9 @@ freqtrade backtesting --ticker-interval 1m #### Using a different on-disk ticker-data source +Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory. +You can then use this data for backtesting as follows: + ```bash freqtrade backtesting --datadir user_data/data/bittrex-20180101 ``` diff --git a/docs/deprecated.md b/docs/deprecated.md index 9fd27c3f9..3ddddc868 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -6,11 +6,11 @@ and are no longer supported. Please avoid their usage in your configuration. ### the `--refresh-pairs-cached` command line option -`--refresh-pairs-cached` in the context of backtesting, hyperopt and edge allowes to refresh candle data for backtesting. -Since this leads to much confusion, and slows down backtesting (while not beeing part of backtesting) this has been singled out +`--refresh-pairs-cached` in the context of backtesting, hyperopt and edge allows to refresh candle data for backtesting. +Since this leads to much confusion, and slows down backtesting (while not being part of backtesting) this has been singled out as a seperate freqtrade subcommand `freqtrade download-data`. -This command was deprecated in `2019.7-dev` and will be removed after the next release. +This command line option was deprecated in `2019.7-dev` and will be removed after the next release. ## Removed features @@ -18,3 +18,9 @@ This command was deprecated in `2019.7-dev` and will be removed after the next r This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) and in freqtrade 2019.7 (master branch). + +### the `--live` command line option + +`--live` in the context of backtesting allowed to download the latest tick data for backtesting. +Did only download the latest 500 candles, so was ineffective in getting good backtest data. +Removed in `2019-7-dev` (develop branch) and in freqtrade 2019-8 (master branch) diff --git a/docs/edge.md b/docs/edge.md index 60cdfd830..d91522770 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -234,7 +234,7 @@ An example of its output: ### Update cached pairs with the latest data -Edge requires historic data the same way than backtesting does. +Edge requires historic data the same way as backtesting does. Please refer to the [download section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) of the documentation for details. ### Precising stoploss range From be308ff91428d8b4b53cc3f5a199289d5652279e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Aug 2019 09:45:28 +0200 Subject: [PATCH 249/269] Fix grammar error in documentation --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 3712fddce..48f658ab9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -5,7 +5,7 @@ Backtesting. ## Getting data for backtesting and hyperopt -To download backtesting data (candles / OHLCV) and hyperoptimization using the `freqtrade download-data` command. +To download backtesting data (candles / OHLCV) and hyperoptimization use the `freqtrade download-data` command. If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes. Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. From 91e72ba081855851eee76df99989ddfd181134ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Aug 2019 19:32:26 +0200 Subject: [PATCH 250/269] small formatting issue --- docs/deprecated.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecated.md b/docs/deprecated.md index 3ddddc868..1460ace33 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -23,4 +23,4 @@ and in freqtrade 2019.7 (master branch). `--live` in the context of backtesting allowed to download the latest tick data for backtesting. Did only download the latest 500 candles, so was ineffective in getting good backtest data. -Removed in `2019-7-dev` (develop branch) and in freqtrade 2019-8 (master branch) +Removed in 2019-7-dev (develop branch) and in freqtrade 2019-8 (master branch) From 210f66e48bbbd654f61d566eca9224121c9ef147 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Aug 2019 19:34:18 +0200 Subject: [PATCH 251/269] Improve wording --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 48f658ab9..90256283e 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -5,7 +5,7 @@ Backtesting. ## Getting data for backtesting and hyperopt -To download backtesting data (candles / OHLCV) and hyperoptimization use the `freqtrade download-data` command. +To download data (candles / OHLCV) needed for backtesting and hyperoptimization use the `freqtrade download-data` command. If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes. Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. From 14aaf8976f6aefc48f99018cc7bc732339151f74 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 21 Aug 2019 02:26:58 +0300 Subject: [PATCH 252/269] fix download replacement script --- scripts/download_backtest_data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 496f83c7d..a8f919a10 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -1,8 +1,10 @@ +#!/usr/bin/env python3 + import sys print("This script has been integrated into freqtrade " - "and it's functionality is available by calling `freqtrade download-data`.") + "and its functionality is available by calling `freqtrade download-data`.") print("Please check the documentation on https://www.freqtrade.io/en/latest/backtesting/ " "for details.") From 75b2db4424dc40e1c9dccfb582479a08bf1aa964 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Aug 2019 06:58:56 +0200 Subject: [PATCH 253/269] FIx loading pairs-list --- freqtrade/configuration/configuration.py | 16 +++++++++------- freqtrade/misc.py | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 329058cef..b3f54d59e 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -346,10 +346,9 @@ class Configuration(object): # or if pairs file is specified explicitely if not pairs_file.exists(): raise OperationalException(f'No pairs file found with path "{pairs_file}".') - - config['pairs'] = json_load(pairs_file) - - config['pairs'].sort() + with pairs_file.open('r') as f: + config['pairs'] = json_load(f) + config['pairs'].sort() return if "config" in self.args and self.args.config: @@ -357,7 +356,10 @@ class Configuration(object): config['pairs'] = config.get('exchange', {}).get('pair_whitelist') else: # Fall back to /dl_path/pairs.json - pairs_file = Path(config['datadir']) / "pairs.json" + pairs_file = Path(config['datadir']) / config['exchange']['name'].lower() / "pairs.json" + print(config['datadir']) if pairs_file.exists(): - config['pairs'] = json_load(pairs_file) - config['pairs'].sort() + with pairs_file.open('r') as f: + config['pairs'] = json_load(f) + if 'pairs' in config: + config['pairs'].sort() diff --git a/freqtrade/misc.py b/freqtrade/misc.py index d01d6a254..12a90a14d 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -6,6 +6,7 @@ import logging import re from datetime import datetime from pathlib import Path +from typing.io import IO import numpy as np import rapidjson @@ -60,7 +61,7 @@ def file_dump_json(filename: Path, data, is_zip=False) -> None: logger.debug(f'done json to "{filename}"') -def json_load(datafile): +def json_load(datafile: IO): """ load data with rapidjson Use this to have a consistent experience, From 13ffb392457269bee56977dbc73d70752b76d531 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Aug 2019 06:59:07 +0200 Subject: [PATCH 254/269] Adjust tests to fixed loading method --- freqtrade/tests/test_configuration.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 5cfee0698..fc3adad44 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -770,6 +770,7 @@ def test_pairlist_resolving_with_config_pl(mocker, default_conf): load_mock = mocker.patch("freqtrade.configuration.configuration.json_load", MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) arglist = [ '--config', 'config.json', @@ -808,6 +809,7 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): def test_pairlist_resolving_fallback(mocker): mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) mocker.patch("freqtrade.configuration.configuration.json_load", MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) arglist = [ From 949ca1abf831bdc0f63165f428651fd813d4d16d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Aug 2019 06:53:51 +0200 Subject: [PATCH 255/269] Fail travis if doc-test fails --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a452d245b..308895591 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,11 @@ jobs: - cp config.json.example config.json - freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 name: hyperopt - - script: flake8 freqtrade scripts + - script: + - flake8 freqtrade scripts + # Test Documentation boxes - + # !!! : is not allowed! + - grep -Er '^!!!\s\S+:' docs/*; test $? -ne 0 name: flake8 - script: mypy freqtrade scripts name: mypy From df1f57392c986963bfda7e835c1119a6cc4367bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Aug 2019 06:56:41 +0200 Subject: [PATCH 256/269] use seperate job for doc test --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 308895591..c75049276 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,12 +34,13 @@ jobs: - cp config.json.example config.json - freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 name: hyperopt + - script: flake8 freqtrade scripts + name: flake8 - script: - - flake8 freqtrade scripts # Test Documentation boxes - # !!! : is not allowed! - - grep -Er '^!!!\s\S+:' docs/*; test $? -ne 0 - name: flake8 + - grep -Er '^!{3}\s\S+:' docs/*; test $? -ne 0 + name: doc syntax - script: mypy freqtrade scripts name: mypy From fe12d2e3b705ea1acc8973ee1f3fa21855d914da Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Aug 2019 06:57:32 +0200 Subject: [PATCH 257/269] Fix documentation syntax --- docs/bot-usage.md | 3 ++- docs/installation.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 27451bcdb..49b152ae6 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -2,7 +2,7 @@ This page explains the different parameters of the bot and how to run it. -!!! Note: +!!! Note If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. @@ -141,6 +141,7 @@ Learn more about strategy file in This parameter allows you to add an additional strategy lookup path, which gets checked before the default locations (The passed path must be a directory!): + ```bash freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` diff --git a/docs/installation.md b/docs/installation.md index 589d3fe7f..f15cc356c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -227,7 +227,7 @@ Freqtrade can also be installed using Anaconda (or Miniconda). conda env create -f environment.yml ``` -!!! Note: +!!! Note This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. ## Windows From 81925dfadf1317962143491926133521f509dd82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Aug 2019 13:01:10 +0200 Subject: [PATCH 258/269] Fix some doc inconsistencies --- docs/plotting.md | 4 +--- docs/strategy-customization.md | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index ac0b1f7cf..c727913b0 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -44,11 +44,9 @@ python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH To plot a timerange (to zoom in): ``` bash -python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 +python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=20180801-20180805 ``` -Timerange doesn't work with live data. - To plot trades stored in a database use `--db-url` argument: ``` bash diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index d71ebfded..9e32ded18 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -319,7 +319,6 @@ if self.dp: print(f"available {pair}, {ticker}") ``` - #### Get data for non-tradeable pairs Data for additional, informative pairs (reference pairs) can be beneficial for some strategies. From b2ef8f4e14e30629941467e6b496378dc12ea243 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Aug 2019 15:26:18 +0200 Subject: [PATCH 259/269] Add additional header --- docs/deprecated.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/deprecated.md b/docs/deprecated.md index 1460ace33..ed70b1936 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,6 +4,8 @@ This page contains description of the command line arguments, configuration para and the bot features that were declared as DEPRECATED by the bot development team and are no longer supported. Please avoid their usage in your configuration. +## Deprecated + ### the `--refresh-pairs-cached` command line option `--refresh-pairs-cached` in the context of backtesting, hyperopt and edge allows to refresh candle data for backtesting. From 0e81d7204c763ecea58378b727aa678f916902bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Aug 2019 15:43:39 +0200 Subject: [PATCH 260/269] Clense jupyter notebook --- .../notebooks/strategy_analysis_example.ipynb | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/user_data/notebooks/strategy_analysis_example.ipynb index bb35d3e91..014f4ca90 100644 --- a/user_data/notebooks/strategy_analysis_example.ipynb +++ b/user_data/notebooks/strategy_analysis_example.ipynb @@ -65,21 +65,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'freqtrade'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# Load data using values set above\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mpathlib\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mPath\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32mfrom\u001b[0m \u001b[0mfreqtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhistory\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mload_pair_history\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m candles = load_pair_history(datadir=data_location,\n", - "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'freqtrade'" - ] - } - ], + "outputs": [], "source": [ "# Load data using values set above\n", "from pathlib import Path\n", @@ -104,23 +92,11 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "scrolled": true }, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'freqtrade'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# Load strategy using values set above\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32mfrom\u001b[0m \u001b[0mfreqtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresolvers\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mStrategyResolver\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m strategy = StrategyResolver({'strategy': strategy_name,\n\u001b[1;32m 4\u001b[0m \u001b[0;34m'user_data_dir'\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0muser_data_dir\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m 'strategy_path': strategy_location}).strategy\n", - "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'freqtrade'" - ] - } - ], + "outputs": [], "source": [ "# Load strategy using values set above\n", "from freqtrade.resolvers import StrategyResolver\n", From d19b11a00f135225022f02d87ac486abac32fd28 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 22 Aug 2019 20:01:41 +0300 Subject: [PATCH 261/269] exchange cosmetics --- freqtrade/exchange/exchange.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7aafff96a..a8e974991 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -772,20 +772,20 @@ class Exchange(object): raise OperationalException(e) from e -def is_exchange_bad(exchange: str) -> bool: - return exchange in BAD_EXCHANGES +def is_exchange_bad(exchange_name: str) -> bool: + return exchange_name in BAD_EXCHANGES -def get_exchange_bad_reason(exchange: str) -> str: - return BAD_EXCHANGES.get(exchange, "") +def get_exchange_bad_reason(exchange_name: str) -> str: + return BAD_EXCHANGES.get(exchange_name, "") -def is_exchange_available(exchange: str, ccxt_module=None) -> bool: - return exchange in available_exchanges(ccxt_module) +def is_exchange_available(exchange_name: str, ccxt_module=None) -> bool: + return exchange_name in available_exchanges(ccxt_module) -def is_exchange_officially_supported(exchange: str) -> bool: - return exchange in ['bittrex', 'binance'] +def is_exchange_officially_supported(exchange_name: str) -> bool: + return exchange_name in ['bittrex', 'binance'] def available_exchanges(ccxt_module=None) -> List[str]: From 782f4112cd33b1ae4a86eac58fd57bf1d8db1c5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Aug 2019 19:49:30 +0200 Subject: [PATCH 262/269] Add test checking stoploss == 0 values --- freqtrade/tests/test_configuration.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index a2a87c2cf..838f0372f 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -648,6 +648,12 @@ def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> N def test_validate_tsl(default_conf): + default_conf['stoploss'] = 0.0 + with pytest.raises(OperationalException, match='The config stoploss needs to be more ' + 'than 0 to avoid problems with sell orders.'): + validate_config_consistency(default_conf) + default_conf['stoploss'] = -0.10 + default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0 default_conf['trailing_stop_positive_offset'] = 0 @@ -669,6 +675,15 @@ def test_validate_tsl(default_conf): default_conf['trailing_stop_positive_offset'] = 0.015 validate_config_consistency(default_conf) + # 0 trailing stop positive - results in "Order would trigger immediately" + default_conf['trailing_stop_positive'] = 0 + default_conf['trailing_stop_positive_offset'] = 0.02 + default_conf['trailing_only_offset_is_reached'] = False + with pytest.raises(OperationalException, + match='The config trailing_stop_positive needs to be more than 0' + 'to avoid problems with sell orders'): + validate_config_consistency(default_conf) + def test_validate_edge(edge_conf): edge_conf.update({"pairlist": { From 70ebd09de4ac628bed224fcadc7a52c4bd9898d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Aug 2019 19:49:50 +0200 Subject: [PATCH 263/269] Add checks verifying that stoploss is not 0 (and positive-stoploss is also not 0). --- freqtrade/configuration/config_validation.py | 15 +++++++++++++-- freqtrade/tests/test_configuration.py | 6 +++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 92846b704..338b8959a 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -68,6 +68,10 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: + if conf.get('stoploss') == 0.0: + raise OperationalException( + 'The config stoploss needs to be different from 0 to avoid problems with sell orders.' + ) # Skip if trailing stoploss is not activated if not conf.get('trailing_stop', False): return @@ -79,13 +83,20 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if tsl_only_offset: if tsl_positive == 0.0: raise OperationalException( - f'The config trailing_only_offset_is_reached needs ' + 'The config trailing_only_offset_is_reached needs ' 'trailing_stop_positive_offset to be more than 0 in your config.') if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: raise OperationalException( - f'The config trailing_stop_positive_offset needs ' + 'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive_offset in your config.') + # Fetch again without default + if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0: + raise OperationalException( + 'The config trailing_stop_positive needs to be different from 0 ' + 'to avoid problems with sell orders.' + ) + def _validate_edge(conf: Dict[str, Any]) -> None: """ diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 838f0372f..618bd69cf 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -649,8 +649,8 @@ def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> N def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 - with pytest.raises(OperationalException, match='The config stoploss needs to be more ' - 'than 0 to avoid problems with sell orders.'): + with pytest.raises(OperationalException, match='The config stoploss needs to be different ' + 'from 0 to avoid problems with sell orders.'): validate_config_consistency(default_conf) default_conf['stoploss'] = -0.10 @@ -680,7 +680,7 @@ def test_validate_tsl(default_conf): default_conf['trailing_stop_positive_offset'] = 0.02 default_conf['trailing_only_offset_is_reached'] = False with pytest.raises(OperationalException, - match='The config trailing_stop_positive needs to be more than 0' + match='The config trailing_stop_positive needs to be different from 0 ' 'to avoid problems with sell orders'): validate_config_consistency(default_conf) From 067208bc9dab1f58c40c7abeab67ab1a11ac1ac6 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 24 Aug 2019 00:10:35 +0300 Subject: [PATCH 264/269] make backtesting an attribute of Hyperopt --- freqtrade/optimize/hyperopt.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cf4740f1c..62a6ab27b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -37,7 +37,7 @@ INITIAL_POINTS = 30 MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization -class Hyperopt(Backtesting): +class Hyperopt: """ Hyperopt class, this class contains all the logic to run a hyperopt simulation @@ -46,7 +46,9 @@ class Hyperopt(Backtesting): hyperopt.start() """ def __init__(self, config: Dict[str, Any]) -> None: - super().__init__(config) + self.config = config + self.backtesting = Backtesting(self.config) + self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss @@ -70,10 +72,10 @@ class Hyperopt(Backtesting): # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): - self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore + self.backtesting.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore if hasattr(self.custom_hyperopt, 'populate_sell_trend'): - self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore + self.backtesting.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): @@ -249,22 +251,22 @@ class Hyperopt(Backtesting): """ params = self.get_args(_params) if self.has_space('roi'): - self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) + self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) if self.has_space('buy'): - self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) + self.backtesting.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) if self.has_space('sell'): - self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) + self.backtesting.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) if self.has_space('stoploss'): - self.strategy.stoploss = params['stoploss'] + self.backtesting.strategy.stoploss = params['stoploss'] processed = load(self.tickerdata_pickle) min_date, max_date = get_timeframe(processed) - results = self.backtest( + results = self.backtesting.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, @@ -345,9 +347,9 @@ class Hyperopt(Backtesting): data = load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=self.config['exchange']['pair_whitelist'], - ticker_interval=self.ticker_interval, + ticker_interval=self.backtesting.ticker_interval, refresh_pairs=self.config.get('refresh_pairs', False), - exchange=self.exchange, + exchange=self.backtesting.exchange, timerange=timerange ) @@ -364,15 +366,15 @@ class Hyperopt(Backtesting): (max_date - min_date).days ) - self.strategy.advise_indicators = \ + self.backtesting.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore - preprocessed = self.strategy.tickerdata_to_dataframe(data) + preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) dump(preprocessed, self.tickerdata_pickle) # We don't need exchange instance anymore while running hyperopt - self.exchange = None # type: ignore + self.backtesting.exchange = None # type: ignore self.load_previous_results() From 667a6233108ab2a9ae53865493ee267d7b77921f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 24 Aug 2019 00:10:55 +0300 Subject: [PATCH 265/269] adjust tests --- freqtrade/tests/optimize/test_hyperopt.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index cfd75fd9d..7b525454c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -429,7 +429,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) - hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -441,8 +441,8 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: assert dumper.called # Should be called twice, once for tickerdata, once to save evaluations assert dumper.call_count == 2 - assert hasattr(hyperopt, "advise_sell") - assert hasattr(hyperopt, "advise_buy") + assert hasattr(hyperopt.backtesting, "advise_sell") + assert hasattr(hyperopt.backtesting, "advise_buy") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == default_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -488,7 +488,7 @@ def test_populate_indicators(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", fill_missing=True)} - dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) + dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -502,7 +502,7 @@ def test_buy_strategy_generator(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", fill_missing=True)} - dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) + dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -538,7 +538,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: backtest_result = pd.DataFrame.from_records(trades, columns=labels) mocker.patch( - 'freqtrade.optimize.hyperopt.Hyperopt.backtest', + 'freqtrade.optimize.hyperopt.Backtesting.backtest', MagicMock(return_value=backtest_result) ) mocker.patch( @@ -644,7 +644,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: }) hyperopt = Hyperopt(default_conf) - hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -681,7 +681,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> }) hyperopt = Hyperopt(default_conf) - hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() From a8842f38ca12f6ebc59f6039206e91079297e13d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Aug 2019 09:08:08 +0200 Subject: [PATCH 266/269] Fix wrong exception message --- freqtrade/configuration/config_validation.py | 2 +- freqtrade/tests/test_configuration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 338b8959a..6a8374e6d 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -88,7 +88,7 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: raise OperationalException( 'The config trailing_stop_positive_offset needs ' - 'to be greater than trailing_stop_positive_offset in your config.') + 'to be greater than trailing_stop_positive in your config.') # Fetch again without default if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0: diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 618bd69cf..10ce7e8cf 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -668,7 +668,7 @@ def test_validate_tsl(default_conf): default_conf['trailing_stop_positive'] = 0.015 with pytest.raises(OperationalException, match=r'The config trailing_stop_positive_offset needs ' - 'to be greater than trailing_stop_positive_offset in your config.'): + 'to be greater than trailing_stop_positive in your config.'): validate_config_consistency(default_conf) default_conf['trailing_stop_positive'] = 0.01 From 3f6eeda3f0e76627e9b62b672d59e7be46ef64f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Aug 2019 18:06:14 +0200 Subject: [PATCH 267/269] Reset stoploss_order_id when recreating fails --- freqtrade/freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e5ecef8bf..e88b9db6a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -662,6 +662,7 @@ class FreqtradeBot(object): return False except DependencyException as exception: + trade.stoploss_order_id = None logger.warning('Unable to place a stoploss order on exchange: %s', exception) # If stoploss order is canceled for some reason we add it @@ -674,6 +675,7 @@ class FreqtradeBot(object): trade.stoploss_order_id = str(stoploss_order_id) return False except DependencyException as exception: + trade.stoploss_order_id = None logger.warning('Stoploss order was cancelled, ' 'but unable to recreate one: %s', exception) @@ -726,7 +728,8 @@ class FreqtradeBot(object): )['id'] trade.stoploss_order_id = str(stoploss_order_id) except DependencyException: - logger.exception(f"Could create trailing stoploss order " + trade.stoploss_order_id = None + logger.exception(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") def _check_and_execute_sell(self, trade: Trade, sell_rate: float, From 365b9c3e9c0f74de5a2ad7eacabd1c325396788a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Aug 2019 18:06:33 +0200 Subject: [PATCH 268/269] Add test to correctly handle unsuccessfull ordercreation --- freqtrade/tests/test_freqtradebot.py | 40 +++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 24d070d2d..dab7a9ff7 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1112,6 +1112,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # Third case: when stoploss was set but it was canceled for some reason # should set a stoploss immediately and return False + caplog.clear() trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 @@ -1127,6 +1128,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # Fourth case: when stoploss is set and it is hit # should unset stoploss_order_id and return true # as a trade actually happened + caplog.clear() freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True @@ -1152,6 +1154,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, ) freqtrade.handle_stoploss_on_exchange(trade) assert log_has('Unable to place a stoploss order on exchange: ', caplog) + assert trade.stoploss_order_id is None # Fifth case: get_order returns InvalidOrder # It should try to add stoploss order @@ -1163,6 +1166,41 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert stoploss_limit.call_count == 1 +def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, + markets, limit_buy_order, limit_sell_order) -> None: + # Sixth case: stoploss order was cancelled but couldn't create new one + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + markets=PropertyMock(return_value=markets), + get_order=MagicMock(return_value={'status': 'canceled'}), + stoploss_limit=MagicMock(side_effect=DependencyException()), + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + freqtrade.create_trades() + trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = '12345' + trade.stoploss_order_id = 100 + assert trade + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert log_has_re(r'Stoploss order was cancelled, but unable to recreate one.*', caplog) + assert trade.stoploss_order_id is None + assert trade.is_open is True + + def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, markets, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set @@ -1324,7 +1362,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 - assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*", caplog) + assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, From 44780837f1d991f5e0ba0be6c7cc12f5beb336af Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Aug 2019 06:33:10 +0200 Subject: [PATCH 269/269] Version bump to 2019-8 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 14f0bb819..1706d610f 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.7-dev' +__version__ = '2019.8' class DependencyException(Exception):