From cd2336887cc74b6a518d84f32d7aa6f1691b4824 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 4 Sep 2019 17:14:04 +0200 Subject: [PATCH 001/319] Add first version with shared parent parsers --- freqtrade/configuration/arguments.py | 65 ++++++++++++++++------------ 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 6e2ecea2e..124bf3b75 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -12,7 +12,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"] +ARGS_MAIN = ARGS_STRATEGY + ["db_url", "sd_notify"] ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount"] @@ -51,11 +51,6 @@ class Arguments: def __init__(self, args: Optional[List[str]]) -> None: self.args = args self._parsed_arg: Optional[argparse.Namespace] = None - self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') - - def _load_args(self) -> None: - self._build_args(optionlist=ARGS_MAIN) - self._build_subcommands() def get_parsed_arg(self) -> Dict[str, Any]: """ @@ -63,7 +58,7 @@ class Arguments: :return: List[str] List of arguments """ if self._parsed_arg is None: - self._load_args() + self._build_subcommands() self._parsed_arg = self._parse_args() return vars(self._parsed_arg) @@ -86,7 +81,6 @@ class Arguments: return parsed_arg def _build_args(self, optionlist, parser=None): - parser = parser or self.parser for val in optionlist: opt = AVAILABLE_CLI_OPTIONS[val] @@ -97,61 +91,76 @@ class Arguments: Builds and attaches all subcommands. :return: None """ + # Build shared arguments (as group Common Options) + _common_parser = argparse.ArgumentParser(add_help=False) + group = _common_parser.add_argument_group("Common Options") + self._build_args(optionlist=ARGS_COMMON, parser=group) + + # Build main command + self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot', + parents=[_common_parser]) + self._build_args(optionlist=ARGS_MAIN, parser=self.parser) + from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.utils import start_create_userdir, start_download_data, start_list_exchanges subparsers = self.parser.add_subparsers(dest='subparser') # Add backtesting subcommand - backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') + backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.', + parents=[_common_parser]) backtesting_cmd.set_defaults(func=start_backtesting) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) # Add edge subcommand - edge_cmd = subparsers.add_parser('edge', help='Edge module.') + edge_cmd = subparsers.add_parser('edge', help='Edge module.', parents=[_common_parser]) edge_cmd.set_defaults(func=start_edge) self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) # Add hyperopt subcommand - hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') + hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.', + parents=[_common_parser], + ) hyperopt_cmd.set_defaults(func=start_hyperopt) self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) # add create-userdir subcommand create_userdir_cmd = subparsers.add_parser('create-userdir', - help="Create user-data directory.") + 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', - help='Print available exchanges.' - ) + list_exchanges_cmd = subparsers.add_parser('list-exchanges', + help='Print available exchanges.', + parents=[_common_parser], + ) 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 = subparsers.add_parser('download-data', + help='Download backtesting data.', + parents=[_common_parser], + ) download_data_cmd.set_defaults(func=start_download_data) self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd) # Add Plotting subcommand from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit - plot_dataframe_cmd = subparsers.add_parser( - 'plot-dataframe', - help='Plot candles with indicators.' - ) + plot_dataframe_cmd = subparsers.add_parser('plot-dataframe', + help='Plot candles with indicators.', + parents=[_common_parser], + ) plot_dataframe_cmd.set_defaults(func=start_plot_dataframe) self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd) # Plot profit - plot_profit_cmd = subparsers.add_parser( - 'plot-profit', - help='Generate plot showing profits.' - ) + plot_profit_cmd = subparsers.add_parser('plot-profit', + help='Generate plot showing profits.', + parents=[_common_parser], + ) plot_profit_cmd.set_defaults(func=start_plot_profit) self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd) From 2a535b72ff6e79b9bc5c218ce97d4b7ec40aede1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 11:15:51 +0200 Subject: [PATCH 002/319] Parser should not have default --- freqtrade/configuration/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 124bf3b75..d26d64c40 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -80,7 +80,7 @@ class Arguments: return parsed_arg - def _build_args(self, optionlist, parser=None): + def _build_args(self, optionlist, parser): for val in optionlist: opt = AVAILABLE_CLI_OPTIONS[val] From cb37f43277bb4d3f81d867888ba7bd63550c2df5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 11:16:14 +0200 Subject: [PATCH 003/319] Add trade subparser (and make subparser a requirement) --- freqtrade/configuration/arguments.py | 14 ++++++++++---- freqtrade/utils.py | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index d26d64c40..ff28a6406 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -99,12 +99,19 @@ class Arguments: # Build main command self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot', parents=[_common_parser]) - self._build_args(optionlist=ARGS_MAIN, parser=self.parser) from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge - from freqtrade.utils import start_create_userdir, start_download_data, start_list_exchanges + from freqtrade.utils import (start_create_userdir, start_download_data, + start_list_exchanges, start_trading) + from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit - subparsers = self.parser.add_subparsers(dest='subparser') + subparsers = self.parser.add_subparsers(dest='subparser', required=True) + + # Add trade subcommand + trade_cmd = subparsers.add_parser('trade', help='Trade module.', + parents=[_common_parser]) + trade_cmd.set_defaults(func=start_trading) + self._build_args(optionlist=ARGS_MAIN, parser=trade_cmd) # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.', @@ -149,7 +156,6 @@ class Arguments: self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd) # Add Plotting subcommand - from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit plot_dataframe_cmd = subparsers.add_parser('plot-dataframe', help='Plot candles with indicators.', parents=[_common_parser], diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 6ce5e888c..8e57606da 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -33,6 +33,17 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str return config +def start_trading(args: Dict[str, Any]) -> int: + """ + Main entry point for trading mode + """ + from freqtrade.worker import Worker + # Load and run worker + worker = Worker(args) + worker.run() + return 0 + + def start_list_exchanges(args: Dict[str, Any]) -> None: """ Print available exchanges @@ -47,7 +58,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: f"{', '.join(available_exchanges())}") -def start_create_userdir(args: Dict[str, Any]) -> None: +def start_create_userdir(args: Dict[str, Any]) -> int: """ Create "user_data" directory to contain user data strategies, hyperopts, ...) :param args: Cli args from Arguments() @@ -57,7 +68,7 @@ def start_create_userdir(args: Dict[str, Any]) -> None: create_userdata_dir(args["user_data_dir"], create_dir=True) else: logger.warning("`create-userdir` requires --userdir to be set.") - sys.exit(1) + return 1 def start_download_data(args: Dict[str, Any]) -> None: From 8664e7f7d35d85dae15862d6d42bf292446bd198 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 11:19:08 +0200 Subject: [PATCH 004/319] Have main.py support only subcommand mode --- freqtrade/main.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 4d6f0dce7..543aab169 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -15,7 +15,6 @@ from typing import Any, List from freqtrade import OperationalException from freqtrade.configuration import Arguments -from freqtrade.worker import Worker logger = logging.getLogger('freqtrade') @@ -33,16 +32,9 @@ def main(sysargv: List[str] = None) -> None: arguments = Arguments(sysargv) args = arguments.get_parsed_arg() - # A subcommand has been issued. - # Means if Backtesting or Hyperopt have been called we exit the bot + # Call subcommand. if 'func' in args: - args['func'](args) - # TODO: fetch return_code as returned by the command function here - return_code = 0 - else: - # Load and run worker - worker = Worker(args) - worker.run() + return_code = args['func'](args) except SystemExit as e: return_code = e From 0f2e277f80b68a82272e62348f59a17418894c9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 11:19:42 +0200 Subject: [PATCH 005/319] Rename subparser variable to command --- freqtrade/configuration/arguments.py | 2 +- tests/test_arguments.py | 4 ++-- tests/test_main.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index ff28a6406..8f6924032 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -105,7 +105,7 @@ class Arguments: start_list_exchanges, start_trading) from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit - subparsers = self.parser.add_subparsers(dest='subparser', required=True) + subparsers = self.parser.add_subparsers(dest='command', required=True) # Add trade subcommand trade_cmd = subparsers.add_parser('trade', help='Trade module.', diff --git a/tests/test_arguments.py b/tests/test_arguments.py index cf0104c01..1b04d9419 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -106,7 +106,7 @@ def test_parse_args_backtesting_custom() -> None: call_args = Arguments(args).get_parsed_arg() assert call_args["config"] == ['test_conf.json'] assert call_args["verbosity"] == 0 - assert call_args["subparser"] == 'backtesting' + assert call_args["command"] == 'backtesting' assert call_args["func"] is not None assert call_args["ticker_interval"] == '1m' assert type(call_args["strategy_list"]) is list @@ -124,7 +124,7 @@ def test_parse_args_hyperopt_custom() -> None: assert call_args["config"] == ['test_conf.json'] assert call_args["epochs"] == 20 assert call_args["verbosity"] == 0 - assert call_args["subparser"] == 'hyperopt' + assert call_args["command"] == 'hyperopt' assert call_args["spaces"] == ['buy'] assert call_args["func"] is not None assert callable(call_args["func"]) diff --git a/tests/test_main.py b/tests/test_main.py index d73edc0da..10d7d3216 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -29,7 +29,7 @@ def test_parse_args_backtesting(mocker) -> None: call_args = backtesting_mock.call_args[0][0] assert call_args["config"] == ['config.json'] assert call_args["verbosity"] == 0 - assert call_args["subparser"] == 'backtesting' + assert call_args["command"] == 'backtesting' assert call_args["func"] is not None assert callable(call_args["func"]) assert call_args["ticker_interval"] is None @@ -45,7 +45,7 @@ def test_main_start_hyperopt(mocker) -> None: call_args = hyperopt_mock.call_args[0][0] assert call_args["config"] == ['config.json'] assert call_args["verbosity"] == 0 - assert call_args["subparser"] == 'hyperopt' + assert call_args["command"] == 'hyperopt' assert call_args["func"] is not None assert callable(call_args["func"]) From 03add90c9494e0a848b73b31e73ecaa05763cfcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 13:18:52 +0200 Subject: [PATCH 006/319] Adjust some tests to new call-method --- tests/optimize/test_backtesting.py | 12 ++++++------ tests/test_main.py | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index fa40809d8..1a52f851e 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -169,9 +169,9 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> patched_configuration_load_config_file(mocker, default_conf) args = [ + 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', - 'backtesting' ] config = setup_configuration(get_args(args), RunMode.BACKTEST) @@ -202,10 +202,10 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> ) args = [ + 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', - 'backtesting', '--ticker-interval', '1m', '--enable-position-stacking', '--disable-max-market-positions', @@ -250,9 +250,9 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog patched_configuration_load_config_file(mocker, default_conf) args = [ + 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', - 'backtesting' ] with pytest.raises(DependencyException, match=r'.*stake amount.*'): @@ -267,9 +267,9 @@ def test_start(mocker, fee, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) args = [ + 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', - 'backtesting' ] args = get_args(args) start_backtesting(args) @@ -812,10 +812,10 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): patched_configuration_load_config_file(mocker, default_conf) args = [ + 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', '--datadir', str(testdatadir), - 'backtesting', '--ticker-interval', '1m', '--timerange', '-100', '--enable-position-stacking', @@ -859,9 +859,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patched_configuration_load_config_file(mocker, default_conf) args = [ + 'backtesting', '--config', 'config.json', '--datadir', str(testdatadir), - 'backtesting', '--ticker-interval', '1m', '--timerange', '-100', '--enable-position-stacking', diff --git a/tests/test_main.py b/tests/test_main.py index 10d7d3216..c5e1c8901 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -58,7 +58,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - args = ['-c', 'config.json.example'] + args = ['trade', '-c', 'config.json.example'] # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): @@ -75,7 +75,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - args = ['-c', 'config.json.example'] + args = ['trade', '-c', 'config.json.example'] # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): @@ -95,7 +95,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - args = ['-c', 'config.json.example'] + args = ['trade', '-c', 'config.json.example'] # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): @@ -114,15 +114,15 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: OperationalException("Oh snap!")]) mocker.patch('freqtrade.worker.Worker._worker', worker_mock) patched_configuration_load_config_file(mocker, default_conf) - reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock()) + reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - args = Arguments(['-c', 'config.json.example']).get_parsed_arg() + args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) with pytest.raises(SystemExit): - main(['-c', 'config.json.example']) + main(['trade', '-c', 'config.json.example']) assert log_has('Using config: config.json.example ...', caplog) assert worker_mock.call_count == 4 @@ -141,7 +141,7 @@ def test_reconfigure(mocker, default_conf) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - args = Arguments(['-c', 'config.json.example']).get_parsed_arg() + args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) freqtrade = worker.freqtrade From 1b25b5f590e3d50678b853003c9ca06e77ef2a82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 13:19:05 +0200 Subject: [PATCH 007/319] Remove duplicate short-form `-s` --- freqtrade/configuration/cli_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index cb07dbdba..24230f1b3 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -171,7 +171,7 @@ AVAILABLE_CLI_OPTIONS = { default=constants.HYPEROPT_EPOCH, ), "spaces": Arg( - '-s', '--spaces', + '--spaces', help='Specify which parameters to hyperopt. Space-separated list. ' 'Default: `%(default)s`.', choices=['all', 'buy', 'sell', 'roi', 'stoploss'], From d62a4d3566eee173e923f51d76cf1d2e5c0e914b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 13:19:21 +0200 Subject: [PATCH 008/319] Fix some minor problems --- freqtrade/configuration/arguments.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 8f6924032..d0db162b9 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -14,8 +14,8 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_MAIN = ARGS_STRATEGY + ["db_url", "sd_notify"] -ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", - "max_open_trades", "stake_amount"] +ARGS_COMMON_OPTIMIZE = ARGS_STRATEGY + ["ticker_interval", "timerange", + "max_open_trades", "stake_amount"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "strategy_list", "export", "exportfilename"] @@ -75,7 +75,7 @@ class Arguments: # (see https://bugs.python.org/issue16399) # Allow no-config for certain commands (like downloading / plotting) if (parsed_arg.config is None and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or - not ('subparser' in parsed_arg and parsed_arg.subparser in NO_CONF_REQURIED))): + not ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED))): parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg @@ -120,7 +120,8 @@ class Arguments: self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) # Add edge subcommand - edge_cmd = subparsers.add_parser('edge', help='Edge module.', parents=[_common_parser]) + edge_cmd = subparsers.add_parser('edge', help='Edge module.', + parents=[_common_parser]) edge_cmd.set_defaults(func=start_edge) self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) From db3b974479976273ba54ce919ed07521d3c1311c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 13:19:40 +0200 Subject: [PATCH 009/319] Fix calling sequence --- tests/optimize/test_edge_cli.py | 6 +++--- tests/test_configuration.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 97103da55..1d2faead0 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -15,9 +15,9 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> patched_configuration_load_config_file(mocker, default_conf) args = [ + 'edge', '--config', 'config.json', '--strategy', 'DefaultStrategy', - 'edge' ] config = setup_configuration(get_args(args), RunMode.EDGE) @@ -45,10 +45,10 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N ) args = [ + 'edge', '--config', 'config.json', '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', - 'edge', '--ticker-interval', '1m', '--timerange', ':100', '--stoplosses=-0.01,-0.10,-0.001' @@ -79,9 +79,9 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: patched_configuration_load_config_file(mocker, edge_conf) args = [ + 'edge', '--config', 'config.json', '--strategy', 'DefaultStrategy', - 'edge' ] args = get_args(args) start_edge(args) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 330b82d39..522b1c1ee 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -806,8 +806,8 @@ def test_pairlist_resolving(): def test_pairlist_resolving_with_config(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) arglist = [ - '--config', 'config.json', 'download-data', + '--config', 'config.json', ] args = Arguments(arglist).get_parsed_arg() @@ -820,8 +820,8 @@ def test_pairlist_resolving_with_config(mocker, default_conf): # Override pairs arglist = [ - '--config', 'config.json', 'download-data', + '--config', 'config.json', '--pairs', 'ETH/BTC', 'XRP/BTC', ] @@ -842,8 +842,8 @@ def test_pairlist_resolving_with_config_pl(mocker, default_conf): mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) arglist = [ - '--config', 'config.json', 'download-data', + '--config', 'config.json', '--pairs-file', 'pairs.json', ] @@ -864,8 +864,8 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) arglist = [ - '--config', 'config.json', 'download-data', + '--config', 'config.json', '--pairs-file', 'pairs.json', ] From e8106f379222e9353bdb93dcc1504cc8d5f2b809 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 13:38:26 +0200 Subject: [PATCH 010/319] Fix most tests to have trade as default argument --- tests/test_arguments.py | 22 +++++++++++----------- tests/test_configuration.py | 30 ++++++++++++++++++------------ tests/test_plotting.py | 4 ++-- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 1b04d9419..53602574e 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -25,27 +25,27 @@ def test_parse_args_defaults() -> None: def test_parse_args_config() -> None: - args = Arguments(['-c', '/dev/null']).get_parsed_arg() + args = Arguments(['trade', '-c', '/dev/null']).get_parsed_arg() assert args["config"] == ['/dev/null'] - args = Arguments(['--config', '/dev/null']).get_parsed_arg() + args = Arguments(['trade', '--config', '/dev/null']).get_parsed_arg() assert args["config"] == ['/dev/null'] - args = Arguments(['--config', '/dev/null', + args = Arguments(['trade', '--config', '/dev/null', '--config', '/dev/zero'],).get_parsed_arg() assert args["config"] == ['/dev/null', '/dev/zero'] def test_parse_args_db_url() -> None: - args = Arguments(['--db-url', 'sqlite:///test.sqlite']).get_parsed_arg() + args = Arguments(['trade', '--db-url', 'sqlite:///test.sqlite']).get_parsed_arg() assert args["db_url"] == 'sqlite:///test.sqlite' def test_parse_args_verbose() -> None: - args = Arguments(['-v']).get_parsed_arg() + args = Arguments(['trade', '-v']).get_parsed_arg() assert args["verbosity"] == 1 - args = Arguments(['--verbose']).get_parsed_arg() + args = Arguments(['trade', '--verbose']).get_parsed_arg() assert args["verbosity"] == 1 @@ -67,7 +67,7 @@ def test_parse_args_invalid() -> None: def test_parse_args_strategy() -> None: - args = Arguments(['--strategy', 'SomeStrategy']).get_parsed_arg() + args = Arguments(['trade', '--strategy', 'SomeStrategy']).get_parsed_arg() assert args["strategy"] == 'SomeStrategy' @@ -77,7 +77,7 @@ def test_parse_args_strategy_invalid() -> None: def test_parse_args_strategy_path() -> None: - args = Arguments(['--strategy-path', '/some/path']).get_parsed_arg() + args = Arguments(['trade', '--strategy-path', '/some/path']).get_parsed_arg() assert args["strategy_path"] == '/some/path' @@ -96,8 +96,8 @@ def test_parse_args_backtesting_invalid() -> None: def test_parse_args_backtesting_custom() -> None: args = [ - '-c', 'test_conf.json', 'backtesting', + '-c', 'test_conf.json', '--ticker-interval', '1m', '--strategy-list', 'DefaultStrategy', @@ -115,8 +115,8 @@ def test_parse_args_backtesting_custom() -> None: def test_parse_args_hyperopt_custom() -> None: args = [ - '-c', 'test_conf.json', 'hyperopt', + '-c', 'test_conf.json', '--epochs', '20', '--spaces', 'buy' ] @@ -132,8 +132,8 @@ def test_parse_args_hyperopt_custom() -> None: def test_download_data_options() -> None: args = [ - '--datadir', 'datadir/directory', 'download-data', + '--datadir', 'datadir/directory', '--pairs-file', 'file_with_pairs', '--days', '30', '--exchange', 'binance' diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 522b1c1ee..3c3ad3026 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -65,7 +65,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: def test__args_to_config(caplog): - arg_list = ['--strategy-path', 'TestTest'] + arg_list = ['trade', '--strategy-path', 'TestTest'] args = Arguments(arg_list).get_parsed_arg() configuration = Configuration(args) config = {} @@ -93,7 +93,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = 0 patched_configuration_load_config_file(mocker, default_conf) - args = Arguments([]).get_parsed_arg() + args = Arguments(['trade']).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() @@ -118,7 +118,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: configsmock ) - arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] + arg_list = ['trade', '-c', 'test_conf.json', '--config', 'test2_conf.json', ] args = Arguments(arg_list).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() @@ -184,7 +184,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> default_conf['max_open_trades'] = -1 patched_configuration_load_config_file(mocker, default_conf) - args = Arguments([]).get_parsed_arg() + args = Arguments(['trade']).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() @@ -208,7 +208,7 @@ def test_load_config_file_exception(mocker) -> None: def test_load_config(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, default_conf) - args = Arguments([]).get_parsed_arg() + args = Arguments(['trade']).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() @@ -221,6 +221,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ + 'trade', '--strategy', 'TestStrategy', '--strategy-path', '/some/path', '--db-url', 'sqlite:///someurl', @@ -240,6 +241,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, conf) arglist = [ + 'trade', '--strategy', 'TestStrategy', '--strategy-path', '/some/path' ] @@ -256,6 +258,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, conf) arglist = [ + 'trade', '--strategy', 'TestStrategy', '--strategy-path', '/some/path' ] @@ -272,6 +275,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, conf) arglist = [ + 'trade', '--strategy', 'TestStrategy', '--strategy-path', '/some/path' ] @@ -290,6 +294,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, conf) arglist = [ + 'trade', '--strategy', 'TestStrategy', '--strategy-path', '/some/path' ] @@ -307,7 +312,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None: }) patched_configuration_load_config_file(mocker, default_conf) - args = Arguments([]).get_parsed_arg() + args = Arguments(['trade']).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() @@ -319,6 +324,7 @@ def test_show_info(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ + 'trade', '--strategy', 'TestStrategy', '--db-url', 'sqlite:///tmp/testdb', ] @@ -335,9 +341,9 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> patched_configuration_load_config_file(mocker, default_conf) arglist = [ + 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', - 'backtesting' ] args = Arguments(arglist).get_parsed_arg() @@ -373,11 +379,11 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non lambda x, *args, **kwargs: Path(x) ) arglist = [ + 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', '--userdir', "/tmp/freqtrade", - 'backtesting', '--ticker-interval', '1m', '--enable-position-stacking', '--disable-max-market-positions', @@ -424,8 +430,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non patched_configuration_load_config_file(mocker, default_conf) arglist = [ - '--config', 'config.json', 'backtesting', + '--config', 'config.json', '--ticker-interval', '1m', '--export', '/bar/foo', '--strategy-list', @@ -552,7 +558,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: # Prevent setting loggers mocker.patch('freqtrade.loggers._set_loggers', MagicMock) - arglist = ['-vvv'] + arglist = ['trade', '-vvv'] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) @@ -604,7 +610,7 @@ def test_set_logfile(default_conf, mocker): patched_configuration_load_config_file(mocker, default_conf) arglist = [ - '--logfile', 'test_file.log', + 'trade', '--logfile', 'test_file.log', ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) @@ -620,7 +626,7 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: default_conf['forcebuy_enable'] = True patched_configuration_load_config_file(mocker, default_conf) - args = Arguments([]).get_parsed_arg() + args = Arguments(['trade']).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 9028ab961..cf5a22973 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -281,8 +281,8 @@ def test_generate_profit_graph(testdatadir): def test_start_plot_dataframe(mocker): aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock()) args = [ - "--config", "config.json.example", "plot-dataframe", + "--config", "config.json.example", "--pairs", "ETH/BTC" ] start_plot_dataframe(get_args(args)) @@ -323,8 +323,8 @@ def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir): def test_start_plot_profit(mocker): aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock()) args = [ - "--config", "config.json.example", "plot-profit", + "--config", "config.json.example", "--pairs", "ETH/BTC" ] start_plot_profit(get_args(args)) From ad2fa61765f80e79d8014255887c79343e642117 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 13:40:28 +0200 Subject: [PATCH 011/319] Fix utils test --- freqtrade/utils.py | 2 +- tests/test_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 8e57606da..de167671f 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -68,7 +68,7 @@ def start_create_userdir(args: Dict[str, Any]) -> int: create_userdata_dir(args["user_data_dir"], create_dir=True) else: logger.warning("`create-userdir` requires --userdir to be set.") - return 1 + sys.exit(1) def start_download_data(args: Dict[str, Any]) -> None: diff --git a/tests/test_utils.py b/tests/test_utils.py index c99044610..7922bb624 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -13,7 +13,7 @@ from tests.conftest import get_args, log_has, patch_exchange def test_setup_utils_configuration(): args = [ - '--config', 'config.json.example', + 'list-exchanges', '--config', 'config.json.example', ] config = setup_utils_configuration(get_args(args), RunMode.OTHER) From 0aa73d5b35b09e088e1ab06dfe148702513de964 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Sep 2019 13:47:33 +0200 Subject: [PATCH 012/319] Add test for failing case --- tests/test_arguments.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 53602574e..8ea55dd6a 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103 import argparse +import re import pytest @@ -8,8 +9,16 @@ from freqtrade.configuration.cli_options import check_int_positive # Parse common command-line-arguments. Used for all tools -def test_parse_args_none() -> None: +def test_parse_args_error(capsys) -> None: arguments = Arguments([]) + with pytest.raises(SystemExit): + arguments.get_parsed_arg() + captured = capsys.readouterr() + assert re.search(r".*the following arguments are required.*", captured.err) + + +def test_parse_args_none() -> None: + arguments = Arguments(['trade']) assert isinstance(arguments, Arguments) x = arguments.get_parsed_arg() assert isinstance(x, dict) @@ -17,7 +26,7 @@ def test_parse_args_none() -> None: def test_parse_args_defaults() -> None: - args = Arguments([]).get_parsed_arg() + args = Arguments(['trade']).get_parsed_arg() assert args["config"] == ['config.json'] assert args["strategy_path"] is None assert args["datadir"] is None From 9ef874e979c064d3423f6dd1b75dc971dd96be34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Sep 2019 06:35:37 +0200 Subject: [PATCH 013/319] Add Custom message during transition period --- freqtrade/configuration/arguments.py | 14 +++++++++----- freqtrade/main.py | 7 +++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index d0db162b9..4fa493eea 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -74,8 +74,9 @@ class Arguments: # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) # Allow no-config for certain commands (like downloading / plotting) - if (parsed_arg.config is None and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or - not ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED))): + if ('config' in parsed_arg and parsed_arg.config is None and + ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or + not ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED))): parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg @@ -97,15 +98,18 @@ class Arguments: self._build_args(optionlist=ARGS_COMMON, parser=group) # Build main command - self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot', - parents=[_common_parser]) + self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.utils import (start_create_userdir, start_download_data, start_list_exchanges, start_trading) from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit - subparsers = self.parser.add_subparsers(dest='command', required=True) + subparsers = self.parser.add_subparsers(dest='command', + # Use custom message when no subhandler is added + # shown from `main.py` + # required=True + ) # Add trade subcommand trade_cmd = subparsers.add_parser('trade', help='Trade module.', diff --git a/freqtrade/main.py b/freqtrade/main.py index 543aab169..d984ff487 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -35,6 +35,13 @@ def main(sysargv: List[str] = None) -> None: # Call subcommand. if 'func' in args: return_code = args['func'](args) + else: + # No subcommand was issued. + raise OperationalException( + "Usage of freqtrade requires a subcommand.\n" + "To use the previous behaviour, run freqtrade with `freqtrade trade [...]`.\n" + "To see a full list of options, please use `freqtrade --help`" + ) except SystemExit as e: return_code = e From 09f18d07b06ac4e7d08a902ea2e84e9dc91f31fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Sep 2019 06:44:07 +0200 Subject: [PATCH 014/319] Adjust some hyperopt tests --- tests/optimize/test_hyperopt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c9a112422..8a8e31df3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -68,8 +68,8 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca patched_configuration_load_config_file(mocker, default_conf) args = [ + 'hyperopt', '--config', 'config.json', - 'hyperopt' ] config = setup_configuration(get_args(args), RunMode.HYPEROPT) @@ -99,9 +99,9 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo ) args = [ + 'hyperopt', '--config', 'config.json', '--datadir', '/foo/bar', - 'hyperopt', '--ticker-interval', '1m', '--timerange', ':100', '--enable-position-stacking', @@ -215,8 +215,8 @@ def test_start(mocker, default_conf, caplog) -> None: patch_exchange(mocker) args = [ - '--config', 'config.json', 'hyperopt', + '--config', 'config.json', '--epochs', '5' ] args = get_args(args) @@ -237,8 +237,8 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: patch_exchange(mocker) args = [ - '--config', 'config.json', 'hyperopt', + '--config', 'config.json', '--epochs', '5' ] args = get_args(args) @@ -254,8 +254,8 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: patch_exchange(mocker) args = [ - '--config', 'config.json', 'hyperopt', + '--config', 'config.json', '--epochs', '5' ] args = get_args(args) From 67b82638dbda9d879987f2012c2d1847c7364c61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Sep 2019 06:44:20 +0200 Subject: [PATCH 015/319] Move test without command to test_main --- tests/test_arguments.py | 11 ++--------- tests/test_main.py | 8 +++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 8ea55dd6a..8cd24fe55 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -4,19 +4,12 @@ import re import pytest +from freqtrade import OperationalException from freqtrade.configuration import Arguments from freqtrade.configuration.cli_options import check_int_positive # Parse common command-line-arguments. Used for all tools -def test_parse_args_error(capsys) -> None: - arguments = Arguments([]) - with pytest.raises(SystemExit): - arguments.get_parsed_arg() - captured = capsys.readouterr() - assert re.search(r".*the following arguments are required.*", captured.err) - - def test_parse_args_none() -> None: arguments = Arguments(['trade']) assert isinstance(arguments, Arguments) @@ -157,8 +150,8 @@ def test_download_data_options() -> None: def test_plot_dataframe_options() -> None: args = [ - '-c', 'config.json.example', 'plot-dataframe', + '-c', 'config.json.example', '--indicators1', 'sma10', 'sma100', '--indicators2', 'macd', 'fastd', 'fastk', '--plot-limit', '30', diff --git a/tests/test_main.py b/tests/test_main.py index c5e1c8901..dac960886 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -11,10 +11,16 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State from freqtrade.worker import Worker -from tests.conftest import (log_has, patch_exchange, +from tests.conftest import (log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) +def test_parse_args_None(caplog) -> None: + with pytest.raises(SystemExit): + main([]) + assert log_has_re(r"Usage of freqtrade requires a subcommand\.", caplog) + + def test_parse_args_backtesting(mocker) -> None: """ Test that main() can start backtesting and also ensure we can pass some specific arguments From 014881e5504ad7b7ca02aa262e007ed23affc22c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Sep 2019 06:44:39 +0200 Subject: [PATCH 016/319] Allow query version without subcommand --- freqtrade/configuration/arguments.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 4fa493eea..fecf1da07 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -99,6 +99,7 @@ class Arguments: # Build main command self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') + self._build_args(optionlist=['version'], parser=self.parser) from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.utils import (start_create_userdir, start_download_data, From 0d13e2cb2eabac2011b156d7a6c454c7defde9f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Sep 2019 11:34:44 +0200 Subject: [PATCH 017/319] Update travis to run new methods --- .travis.yml | 4 ++-- freqtrade/utils.py | 2 +- tests/test_arguments.py | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 405228ab8..81c3de2fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,11 +28,11 @@ jobs: name: pytest - script: - cp config.json.example config.json - - freqtrade --datadir tests/testdata backtesting + - freqtrade backtesting --datadir tests/testdata name: backtest - script: - cp config.json.example config.json - - freqtrade --datadir tests/testdata hyperopt -e 5 + - freqtrade hyperopt --datadir tests/testdata -e 5 name: hyperopt - script: flake8 name: flake8 diff --git a/freqtrade/utils.py b/freqtrade/utils.py index de167671f..6b2b5314b 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -58,7 +58,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: f"{', '.join(available_exchanges())}") -def start_create_userdir(args: Dict[str, Any]) -> int: +def start_create_userdir(args: Dict[str, Any]) -> None: """ Create "user_data" directory to contain user data strategies, hyperopts, ...) :param args: Cli args from Arguments() diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 8cd24fe55..4e01732b3 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -1,10 +1,8 @@ # pragma pylint: disable=missing-docstring, C0103 import argparse -import re import pytest -from freqtrade import OperationalException from freqtrade.configuration import Arguments from freqtrade.configuration.cli_options import check_int_positive From 52523bcd8bd9cd6c5b4a02730bb6d8ca50904793 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Sep 2019 16:25:25 +0200 Subject: [PATCH 018/319] Use strategy child parser --- freqtrade/configuration/arguments.py | 29 +++++++++++++++------------- tests/optimize/test_hyperopt.py | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index fecf1da07..d2e5d1fc5 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -12,10 +12,9 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_MAIN = ARGS_STRATEGY + ["db_url", "sd_notify"] +ARGS_TRADE = ["db_url", "sd_notify"] -ARGS_COMMON_OPTIMIZE = ARGS_STRATEGY + ["ticker_interval", "timerange", - "max_open_trades", "stake_amount"] +ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "strategy_list", "export", "exportfilename"] @@ -35,8 +34,9 @@ ARGS_CREATE_USERDIR = ["user_data_dir"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] -ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", - "trade_source", "export", "exportfilename", "timerange", "ticker_interval"] +ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", + "db_url", "trade_source", "export", "exportfilename", + "timerange", "ticker_interval"] ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "ticker_interval"] @@ -94,9 +94,13 @@ class Arguments: """ # Build shared arguments (as group Common Options) _common_parser = argparse.ArgumentParser(add_help=False) - group = _common_parser.add_argument_group("Common Options") + group = _common_parser.add_argument_group("Common arguments") self._build_args(optionlist=ARGS_COMMON, parser=group) + _strategy_parser = argparse.ArgumentParser(add_help=False) + strategy_group = _common_parser.add_argument_group("Strategy arguments") + self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group) + # Build main command self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self._build_args(optionlist=['version'], parser=self.parser) @@ -114,25 +118,25 @@ class Arguments: # Add trade subcommand trade_cmd = subparsers.add_parser('trade', help='Trade module.', - parents=[_common_parser]) + parents=[_common_parser, _strategy_parser]) trade_cmd.set_defaults(func=start_trading) - self._build_args(optionlist=ARGS_MAIN, parser=trade_cmd) + self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd) # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.', - parents=[_common_parser]) + parents=[_common_parser, _strategy_parser]) backtesting_cmd.set_defaults(func=start_backtesting) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.', - parents=[_common_parser]) + parents=[_common_parser, _strategy_parser]) edge_cmd.set_defaults(func=start_edge) self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.', - parents=[_common_parser], + parents=[_common_parser, _strategy_parser], ) hyperopt_cmd.set_defaults(func=start_hyperopt) self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) @@ -140,7 +144,6 @@ class Arguments: # add create-userdir subcommand 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) @@ -164,7 +167,7 @@ class Arguments: # Add Plotting subcommand plot_dataframe_cmd = subparsers.add_parser('plot-dataframe', help='Plot candles with indicators.', - parents=[_common_parser], + parents=[_common_parser, _strategy_parser], ) plot_dataframe_cmd.set_defaults(func=start_plot_dataframe) self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 8a8e31df3..5ff11d5ea 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -198,8 +198,8 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None patch_exchange(mocker) args = [ - '--config', 'config.json', 'hyperopt', + '--config', 'config.json', '--epochs', '5' ] args = get_args(args) From 381b0d3d07f127ac4f1a27b31ccf997947dba103 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Sep 2019 16:28:14 +0200 Subject: [PATCH 019/319] Fix typo with new parser --- freqtrade/configuration/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index d2e5d1fc5..a8d4b48f1 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -98,7 +98,7 @@ class Arguments: self._build_args(optionlist=ARGS_COMMON, parser=group) _strategy_parser = argparse.ArgumentParser(add_help=False) - strategy_group = _common_parser.add_argument_group("Strategy arguments") + strategy_group = _strategy_parser.add_argument_group("Strategy arguments") self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group) # Build main command From 2710226326f71eaaaf07830f64b33b905c180224 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Sep 2019 19:18:02 +0200 Subject: [PATCH 020/319] Update documentation to use subcommands --- docs/backtesting.md | 2 +- docs/bot-usage.md | 188 ++++++++++++++++++++++++--------- docs/docker.md | 4 +- docs/edge.md | 2 +- docs/faq.md | 2 +- docs/plotting.md | 59 +++++++++-- docs/rest-api.md | 2 +- docs/strategy-customization.md | 6 +- freqtrade.service | 2 +- freqtrade.service.watchdog | 2 +- 10 files changed, 198 insertions(+), 71 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 75aba6c73..6383b1855 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -45,7 +45,7 @@ freqtrade backtesting --datadir user_data/data/bittrex-20180101 #### With a (custom) strategy file ```bash -freqtrade -s SampleStrategy backtesting +freqtrade backtesting -s SampleStrategy ``` Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory. diff --git a/docs/bot-usage.md b/docs/bot-usage.md index f44400e32..ee01780a0 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -5,27 +5,47 @@ This page explains the different parameters of the bot and how to run it. !!! Note If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. - ## Bot commands ``` -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} ... +usage: freqtrade [-h] [-V] + {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,download-data,plot-dataframe,plot-profit} + ... Free, open source crypto trading bot positional arguments: - {backtesting,edge,hyperopt,create-userdir,list-exchanges} + {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,download-data,plot-dataframe,plot-profit} + trade Trade module. backtesting Backtesting module. edge Edge module. hyperopt Hyperopt module. create-userdir Create user-data directory. list-exchanges Print available exchanges. + download-data Download backtesting data. + plot-dataframe Plot candles with indicators. + plot-profit Generate plot showing profits. optional arguments: -h, --help show this help message and exit + -V, --version show program's version number and exit +``` + +### Bot trading commands + +``` +usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + [--db-url PATH] [--sd-notify] + +optional arguments: + -h, --help show this help message and exit + --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. + +Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). --logfile FILE Log to the file specified. -V, --version show program's version number and exit @@ -37,14 +57,12 @@ optional arguments: Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. + +Strategy arguments: -s NAME, --strategy NAME Specify strategy class name (default: `DefaultStrategy`). --strategy-path PATH Specify additional strategy lookup path. - --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. ``` @@ -128,7 +146,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has a strategy class called `AwesomeStrategy` to load it: ```bash -freqtrade --strategy AwesomeStrategy +freqtrade trade --strategy AwesomeStrategy ``` If the bot does not find your strategy file, it will display in an error @@ -143,7 +161,7 @@ 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 +freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory ``` #### How to install a strategy? @@ -167,20 +185,22 @@ freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite Backtesting also uses the config specified via `-c/--config`. ``` -usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--max_open_trades MAX_OPEN_TRADES] - [--stake_amount STAKE_AMOUNT] [-r] [--eps] [--dmmp] - [-l] - [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] - [--export EXPORT] [--export-filename PATH] +usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] [-s NAME] + [--strategy-path PATH] [-i TICKER_INTERVAL] + [--timerange TIMERANGE] [--max_open_trades INT] + [--stake_amount STAKE_AMOUNT] [--eps] [--dmmp] + [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] + [--export EXPORT] [--export-filename PATH] optional arguments: -h, --help show this help message and exit -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL - Specify ticker interval (1m, 5m, 30m, 1h, 1d). + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades MAX_OPEN_TRADES + --max_open_trades INT Specify max_open_trades to use. --stake_amount STAKE_AMOUNT Specify stake_amount. @@ -193,26 +213,47 @@ optional arguments: number). --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to - backtest Please note that ticker-interval needs to be + 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 (so backtest-data.json - becomes backtest-data-DefaultStrategy.json - --export EXPORT Export backtest results, argument are: trades. Example - --export=trades + this together with `--export trades`, the strategy- + name is injected into the filename (so `backtest- + data.json` becomes `backtest-data- + DefaultStrategy.json` + --export EXPORT Export backtest results, argument are: trades. + Example: `--export=trades` --export-filename PATH - Save backtest results to this filename requires - --export to be set as well Example --export- - filename=user_data/backtest_results/backtest_today.json - (default: user_data/backtest_results/backtest- - result.json) + Save backtest results to the file with this filename + (default: `user_data/backtest_results/backtest- + result.json`). Requires `--export` to be set as well. + Example: `--export-filename=user_data/backtest_results + /backtest_today.json` + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. + -V, --version show program's version number and exit + -c PATH, --config PATH + 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 directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name (default: + `DefaultStrategy`). + --strategy-path PATH Specify additional strategy lookup path. + ``` ### Getting historic data for backtesting 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 +Check the corresponding [Data Downloading](data-download.md) section for more details ## Hyperopt commands @@ -220,15 +261,17 @@ To optimize your strategy, you can use hyperopt parameter hyperoptimization to find optimal parameter values for your stategy. ``` -usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] +usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] - [--stake_amount STAKE_AMOUNT] [-r] + [--stake_amount STAKE_AMOUNT] [--customhyperopt NAME] [--hyperopt-path PATH] [--eps] [-e INT] - [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] - [--dmmp] [--print-all] [--no-color] [-j JOBS] - [--random-state INT] [--min-trades INT] [--continue] - [--hyperopt-loss NAME] + [--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] + [--dmmp] [--print-all] [--no-color] [--print-json] + [-j JOBS] [--random-state INT] [--min-trades INT] + [--continue] [--hyperopt-loss NAME] optional arguments: -h, --help show this help message and exit @@ -250,7 +293,7 @@ optional arguments: Allow buying the same pair multiple times (position stacking). -e INT, --epochs INT Specify number of epochs (default: 100). - -s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] + --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] Specify which parameters to hyperopt. Space-separated list. Default: `all`. --dmmp, --disable-max-market-positions @@ -260,6 +303,7 @@ optional arguments: --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. + --print-json Print best result detailization in JSON format. -j JOBS, --job-workers JOBS The number of concurrently running jobs for hyperoptimization (hyperopt worker processes). If -1 @@ -278,8 +322,28 @@ optional arguments: generate completely different results, since the target for optimization is different. Built-in Hyperopt-loss-functions are: DefaultHyperOptLoss, - OnlyProfitHyperOptLoss, SharpeHyperOptLoss. - (default: `DefaultHyperOptLoss`). + OnlyProfitHyperOptLoss, SharpeHyperOptLoss.(default: + `DefaultHyperOptLoss`). + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. + -V, --version show program's version number and exit + -c PATH, --config PATH + 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 directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name (default: + `DefaultStrategy`). + --strategy-path PATH Specify additional strategy lookup path. + ``` ## Edge commands @@ -287,26 +351,48 @@ optional arguments: To know your trade expectancy and winrate against historical data, you can use Edge. ``` -usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--max_open_trades MAX_OPEN_TRADES] - [--stake_amount STAKE_AMOUNT] [-r] - [--stoplosses STOPLOSS_RANGE] +usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + [-i TICKER_INTERVAL] [--timerange TIMERANGE] + [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] + [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL - Specify ticker interval (1m, 5m, 30m, 1h, 1d). + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades MAX_OPEN_TRADES + --max_open_trades INT Specify max_open_trades to use. --stake_amount STAKE_AMOUNT Specify stake_amount. --stoplosses STOPLOSS_RANGE - Defines a range of stoploss against which edge will - assess the strategy the format is "min,max,step" - (without any space).example: - --stoplosses=-0.01,-0.1,-0.001 + Defines a range of stoploss values against which edge + will assess the strategy. The format is "min,max,step" + (without any space). Example: + `--stoplosses=-0.01,-0.1,-0.001` + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. + -V, --version show program's version number and exit + -c PATH, --config PATH + 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 directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name (default: + `DefaultStrategy`). + --strategy-path PATH Specify additional strategy lookup path. + ``` To understand edge and how to read the results, please read the [edge documentation](edge.md). diff --git a/docs/docker.md b/docs/docker.md index 923dec1e2..7fc8d2ba4 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -160,7 +160,7 @@ docker run -d \ -v ~/.freqtrade/config.json:/freqtrade/config.json \ -v ~/.freqtrade/user_data/:/freqtrade/user_data \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy + freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy ``` !!! Note @@ -199,7 +199,7 @@ docker run -d \ -v ~/.freqtrade/config.json:/freqtrade/config.json \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade --strategy AwsomelyProfitableStrategy backtesting + freqtrade backtesting --strategy AwsomelyProfitableStrategy ``` Head over to the [Backtesting Documentation](backtesting.md) for more details. diff --git a/docs/edge.md b/docs/edge.md index d91522770..80db7b91e 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -235,7 +235,7 @@ An example of its output: ### Update cached pairs with the latest data 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. +Please refer to the [Data Downloading](data-download.md) section of the documentation for details. ### Precising stoploss range diff --git a/docs/faq.md b/docs/faq.md index a441ffacd..c519f8cc3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -4,7 +4,7 @@ ### The bot does not start -Running the bot with `freqtrade --config config.json` does show the output `freqtrade: command not found`. +Running the bot with `freqtrade trade --config config.json` does show the output `freqtrade: command not found`. This could have the following reasons: diff --git a/docs/plotting.md b/docs/plotting.md index 4deb6db12..25278c99d 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -23,13 +23,15 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three Possible arguments: ``` -usage: freqtrade plot-dataframe [-h] [-p PAIRS [PAIRS ...]] +usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] [-s NAME] + [--strategy-path PATH] [-p PAIRS [PAIRS ...]] [--indicators1 INDICATORS1 [INDICATORS1 ...]] [--indicators2 INDICATORS2 [INDICATORS2 ...]] [--plot-limit INT] [--db-url PATH] [--trade-source {DB,file}] [--export EXPORT] [--export-filename PATH] - [--timerange TIMERANGE] + [--timerange TIMERANGE] [-i TICKER_INTERVAL] optional arguments: -h, --help show this help message and exit @@ -62,6 +64,28 @@ optional arguments: /backtest_today.json` --timerange TIMERANGE Specify what timerange of data to use. + -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. + -V, --version show program's version number and exit + -c PATH, --config PATH + 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 directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name (default: + `DefaultStrategy`). + --strategy-path PATH Specify additional strategy lookup path. ``` @@ -83,7 +107,7 @@ Use `--indicators1` for the main plot and `--indicators2` for the subplot below You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command. ``` bash -freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma ema --indicators2 macd +freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd ``` ### Further usage examples @@ -91,25 +115,25 @@ freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma To plot multiple pairs, separate them with a space: ``` bash -freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH XRP/ETH +freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH XRP/ETH ``` To plot a timerange (to zoom in) ``` bash -freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --timerange=20180801-20180805 +freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805 ``` To plot trades stored in a database use `--db-url` in combination with `--trade-source DB`: ``` bash -freqtrade --strategy AwesomeStrategy plot-dataframe --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB +freqtrade plot-dataframe --strategy AwesomeStrategy --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB ``` To plot trades from a backtesting result, use `--export-filename ` ``` bash -freqtrade --strategy AwesomeStrategy plot-dataframe --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH +freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH ``` ## Plot profit @@ -133,10 +157,11 @@ The third graph can be useful to spot outliers, events in pairs that cause profi Possible options for the `freqtrade plot-profit` subcommand: ``` -usage: freqtrade plot-profit [-h] [-p PAIRS [PAIRS ...]] +usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] [--timerange TIMERANGE] [--export EXPORT] [--export-filename PATH] [--db-url PATH] - [--trade-source {DB,file}] + [--trade-source {DB,file}] [-i TICKER_INTERVAL] optional arguments: -h, --help show this help message and exit @@ -159,6 +184,22 @@ optional arguments: --trade-source {DB,file} Specify the source for trades (Can be DB or file (backtest file)) Default: file + -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. + -V, --version show program's version number and exit + -c PATH, --config PATH + 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 directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. ``` diff --git a/docs/rest-api.md b/docs/rest-api.md index afecc1d80..5295ebab4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -58,7 +58,7 @@ docker run -d \ -v ~/.freqtrade/user_data/:/freqtrade/user_data \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ -p 127.0.0.1:8080:8080 \ - freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy + freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy ``` !!! Danger "Security warning" diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index b927e5aad..bb7138759 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -13,7 +13,7 @@ Let assume you have a class called `AwesomeStrategy` in the file `awesome-strate 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) ```bash -freqtrade --strategy AwesomeStrategy +freqtrade trade --strategy AwesomeStrategy ``` ## Change your strategy @@ -45,7 +45,7 @@ The current version is 2 - which is also the default when it's not set explicitl Future versions will require this to be set. ```bash -freqtrade --strategy AwesomeStrategy +freqtrade trade --strategy AwesomeStrategy ``` **For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py) @@ -402,7 +402,7 @@ The default buy strategy is located in the file If you want to use a strategy from a different directory you can pass `--strategy-path` ```bash -freqtrade --strategy AwesomeStrategy --strategy-path /some/directory +freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory ``` ### Further strategy ideas diff --git a/freqtrade.service b/freqtrade.service index 9de9f13c7..df220ed39 100644 --- a/freqtrade.service +++ b/freqtrade.service @@ -6,7 +6,7 @@ After=network.target # Set WorkingDirectory and ExecStart to your file paths accordingly # NOTE: %h will be resolved to /home/ WorkingDirectory=%h/freqtrade -ExecStart=/usr/bin/freqtrade +ExecStart=/usr/bin/freqtrade trade Restart=on-failure [Install] diff --git a/freqtrade.service.watchdog b/freqtrade.service.watchdog index ba491fa53..66ea00d76 100644 --- a/freqtrade.service.watchdog +++ b/freqtrade.service.watchdog @@ -6,7 +6,7 @@ After=network.target # Set WorkingDirectory and ExecStart to your file paths accordingly # NOTE: %h will be resolved to /home/ WorkingDirectory=%h/freqtrade -ExecStart=/usr/bin/freqtrade --sd-notify +ExecStart=/usr/bin/freqtrade trade --sd-notify Restart=always #Restart=on-failure From 344a0a094fcaef785d4d58bee58b654561d468dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Sep 2019 19:21:18 +0200 Subject: [PATCH 021/319] Update remaining documentations --- docs/bot-usage.md | 8 ++++---- docs/hyperopt.md | 2 +- docs/installation.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index ee01780a0..112fc78a1 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -72,7 +72,7 @@ The bot allows you to select which configuration file you want to use by means o the `-c/--config` command line option: ```bash -freqtrade -c path/far/far/away/config.json +freqtrade trade -c path/far/far/away/config.json ``` Per default, the bot loads the `config.json` configuration file from the current @@ -91,13 +91,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua require them): ```bash -freqtrade -c ./config.json +freqtrade trade -c ./config.json ``` and specify both configuration files when running in the normal Live Trade Mode: ```bash -freqtrade -c ./config.json -c path/to/secrets/keys.config.json +freqtrade trade -c ./config.json -c path/to/secrets/keys.config.json ``` This could help you hide your private Exchange key and Exchange secrete on you local machine @@ -177,7 +177,7 @@ using `--db-url`. This can also be used to specify a custom database in production mode. Example command: ```bash -freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite +freqtrade trade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite ``` ## Backtesting commands diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 1ca371e3d..e6f753072 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -239,7 +239,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade -c config.json hyperopt --customhyperopt -e 5000 --spaces all +freqtrade hyperopt --config config.json --customhyperopt -e 5000 --spaces all ``` Use `` as the name of the custom hyperopt used. diff --git a/docs/installation.md b/docs/installation.md index 68348d4b0..3d0f27f2a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -187,7 +187,7 @@ python3 -m pip install -e . If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -freqtrade -c config.json +freqtrade trade -c config.json ``` *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. From 52ff391c8a8eab054287683f82f2beae18703ae6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Sep 2019 19:48:37 +0200 Subject: [PATCH 022/319] Default dockerfile to "freqtrade trade" --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8677b54de..5b69f55a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,3 +24,5 @@ RUN pip install numpy --no-cache-dir \ COPY . /freqtrade/ RUN pip install -e . --no-cache-dir ENTRYPOINT ["freqtrade"] +# Default to trade mode +CMD [ "trade" ] From b73426b91f5925431a4631c0618fa917d845fe81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Sep 2019 19:54:44 +0200 Subject: [PATCH 023/319] Disable Defaulting to DefaultStrategy --- .travis.yml | 4 ++-- build_helpers/publish_docker.sh | 2 +- docs/bot-usage.md | 4 ++-- docs/configuration.md | 2 +- freqtrade/configuration/cli_options.py | 3 +-- freqtrade/configuration/configuration.py | 2 +- freqtrade/constants.py | 1 - freqtrade/resolvers/strategy_resolver.py | 7 +++++-- tests/conftest.py | 1 + tests/strategy/test_strategy.py | 12 +++++++++++- tests/test_configuration.py | 1 - 11 files changed, 25 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 81c3de2fb..b066e7044 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,11 +28,11 @@ jobs: name: pytest - script: - cp config.json.example config.json - - freqtrade backtesting --datadir tests/testdata + - freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy name: backtest - script: - cp config.json.example config.json - - freqtrade hyperopt --datadir tests/testdata -e 5 + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy name: hyperopt - script: flake8 name: flake8 diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 839ca0876..b8318c196 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -23,7 +23,7 @@ if [ $? -ne 0 ]; then fi # Run backtest -docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} --datadir /tests/testdata backtesting +docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy DefaultStrategy if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 112fc78a1..2b66d3c25 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -60,8 +60,8 @@ Common arguments: Strategy arguments: -s NAME, --strategy NAME - Specify strategy class name (default: - `DefaultStrategy`). + Specify strategy class name which will be used by the + bot. --strategy-path PATH Specify additional strategy lookup path. ``` diff --git a/docs/configuration.md b/docs/configuration.md index 0d902766a..9c1b9d4f7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -95,7 +95,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. | `initial_state` | running | Defines the initial application state. More information below. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. -| `strategy` | DefaultStrategy | Defines Strategy class to use. +| `strategy` | None | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`. | `strategy_path` | null | Adds an additional strategy lookup path (must be a directory). | `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. diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 24230f1b3..2ecd4cfc5 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -66,9 +66,8 @@ AVAILABLE_CLI_OPTIONS = { # Main options "strategy": Arg( '-s', '--strategy', - help='Specify strategy class name (default: `%(default)s`).', + help='Specify strategy class name which will be used by the bot.', metavar='NAME', - default='DefaultStrategy', ), "strategy_path": Arg( '--strategy-path', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 764593d0f..ac27a5c99 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -128,7 +128,7 @@ class Configuration: self._process_logging_options(config) # Set strategy if not specified in config and or if it's non default - if self.args.get("strategy") != constants.DEFAULT_STRATEGY or not config.get('strategy'): + if self.args.get("strategy") or not config.get('strategy'): config.update({'strategy': self.args.get("strategy")}) self._args_to_config(config, argname='strategy_path', diff --git a/freqtrade/constants.py b/freqtrade/constants.py index abf43b24d..749ae25b5 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -9,7 +9,6 @@ PROCESS_THROTTLE_SECS = 5 # sec DEFAULT_TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec -DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_HYPEROPT = 'DefaultHyperOpts' DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index ca7e1165b..1b6d5c48a 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -32,8 +32,11 @@ class StrategyResolver(IResolver): """ config = config or {} - # Verify the strategy is in the configuration, otherwise fallback to the default strategy - strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY + if not config.get('strategy'): + raise OperationalException("No strategy set. Please use `--strategy` to specify " + "the strategy class to use.") + + strategy_name = config['strategy'] self.strategy: IStrategy = self._load_strategy(strategy_name, config=config, extra_dir=config.get('strategy_path')) diff --git a/tests/conftest.py b/tests/conftest.py index 6a0a74b5b..0ffb5a066 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -242,6 +242,7 @@ def default_conf(testdatadir): "db_url": "sqlite://", "user_data_dir": Path("user_data"), "verbosity": 3, + "strategy": "DefaultStrategy" } return configuration diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 6992d1aa5..82db30d47 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -55,6 +55,7 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): + default_conf['strategy'] = 'SampleStrategy' resolver = StrategyResolver(default_conf) extra_dir = Path.cwd() / 'some/path' resolver._load_strategy('SampleStrategy', config=default_conf, extra_dir=extra_dir) @@ -65,13 +66,22 @@ def test_load_strategy_invalid_directory(result, caplog, default_conf): def test_load_not_found_strategy(default_conf): - strategy = StrategyResolver(default_conf) + default_conf['strategy'] = 'NotFoundStrategy' with pytest.raises(OperationalException, match=r"Impossible to load Strategy 'NotFoundStrategy'. " r"This class does not exist or contains Python code errors."): + strategy = StrategyResolver(default_conf) strategy._load_strategy(strategy_name='NotFoundStrategy', config=default_conf) +def test_load_strategy_noname(default_conf): + default_conf['strategy'] = '' + with pytest.raises(OperationalException, + match="No strategy set. Please use `--strategy` to specify " + "the strategy class to use."): + StrategyResolver(default_conf) + + def test_strategy(result, default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 3c3ad3026..333a8992a 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -212,7 +212,6 @@ def test_load_config(default_conf, mocker) -> None: configuration = Configuration(args) validated_conf = configuration.load_config() - assert validated_conf.get('strategy') == 'DefaultStrategy' assert validated_conf.get('strategy_path') is None assert 'edge' not in validated_conf From 95299d94c4c824eed2c3aca6fc370a8b85633901 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Oct 2019 06:39:24 +0200 Subject: [PATCH 024/319] Remove unused test line --- tests/strategy/test_strategy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 82db30d47..7445e3ca7 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -70,8 +70,7 @@ def test_load_not_found_strategy(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 = StrategyResolver(default_conf) - strategy._load_strategy(strategy_name='NotFoundStrategy', config=default_conf) + StrategyResolver(default_conf) def test_load_strategy_noname(default_conf): From c4105436ebbad89d37922aad57068bef5c5cca32 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 10 Oct 2019 04:37:32 +0300 Subject: [PATCH 025/319] Disable defaulting to DefaultHyperOpts and DefaultHyperOptLoss --- .travis.yml | 2 +- docs/bot-usage.md | 8 +-- freqtrade/configuration/cli_options.py | 7 +- freqtrade/constants.py | 2 - freqtrade/resolvers/hyperopt_resolver.py | 27 +++++--- tests/optimize/test_hyperopt.py | 81 +++++++++++++++++++++--- 6 files changed, 96 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index b066e7044..7a75a76c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ jobs: name: backtest - script: - cp config.json.example config.json - - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy --customhyperopt DefaultHyperOpts --hyperopt-loss DefaultHyperOptLoss name: hyperopt - script: flake8 name: flake8 diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 2b66d3c25..fcf82826a 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -285,8 +285,8 @@ optional arguments: --stake_amount STAKE_AMOUNT Specify stake_amount. --customhyperopt NAME - Specify hyperopt class name (default: - `DefaultHyperOpts`). + Specify hyperopt class name which will be used by the + bot. --hyperopt-path PATH Specify additional lookup path for Hyperopts and Hyperopt Loss functions. --eps, --enable-position-stacking @@ -322,8 +322,8 @@ optional arguments: generate completely different results, since the target for optimization is different. Built-in Hyperopt-loss-functions are: DefaultHyperOptLoss, - OnlyProfitHyperOptLoss, SharpeHyperOptLoss.(default: - `DefaultHyperOptLoss`). + OnlyProfitHyperOptLoss, SharpeHyperOptLoss. + Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 2ecd4cfc5..6928ddfdb 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -153,9 +153,8 @@ AVAILABLE_CLI_OPTIONS = { # Hyperopt "hyperopt": Arg( '--customhyperopt', - help='Specify hyperopt class name (default: `%(default)s`).', + help='Specify hyperopt class name which will be used by the bot.', metavar='NAME', - default=constants.DEFAULT_HYPEROPT, ), "hyperopt_path": Arg( '--hyperopt-path', @@ -232,10 +231,8 @@ AVAILABLE_CLI_OPTIONS = { 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. Built-in Hyperopt-loss-functions are: ' - 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.' - '(default: `%(default)s`).', + 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.', metavar='NAME', - default=constants.DEFAULT_HYPEROPT_LOSS, ), # List exchanges "print_one_column": Arg( diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 749ae25b5..2f490c900 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -9,8 +9,6 @@ PROCESS_THROTTLE_SECS = 5 # sec DEFAULT_TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec -DEFAULT_HYPEROPT = 'DefaultHyperOpts' -DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' UNLIMITED_STAKE_AMOUNT = 'unlimited' diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index e96394d69..45fe2548e 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import Optional, Dict from freqtrade import OperationalException -from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_HYPEROPT_LOSS from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver @@ -20,17 +19,21 @@ class HyperOptResolver(IResolver): """ This class contains all the logic to load custom hyperopt class """ - __slots__ = ['hyperopt'] - def __init__(self, config: Dict) -> None: + def __init__(self, config: Dict = None) -> None: """ Load the custom class from config parameter :param config: configuration dictionary """ + config = config or {} + + if not config.get('hyperopt'): + raise OperationalException("No Hyperopt set. Please use `--customhyperopt` to specify " + "the Hyperopt class to use.") + + hyperopt_name = config['hyperopt'] - # 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, config, extra_dir=config.get('hyperopt_path')) @@ -75,7 +78,6 @@ class HyperOptLossResolver(IResolver): """ This class contains all the logic to load custom hyperopt loss class """ - __slots__ = ['hyperoptloss'] def __init__(self, config: Dict = None) -> None: @@ -85,17 +87,22 @@ class HyperOptLossResolver(IResolver): """ config = config or {} - # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt - hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS + if not config.get('hyperopt_loss'): + raise OperationalException("No Hyperopt Loss Function set. Please use " + "`--hyperopt-loss` to specify " + "the Hyperopt Loss Function class to use.") + + hyperoptloss_name = config['hyperopt_loss'] + self.hyperoptloss = self._load_hyperoptloss( - hyperopt_name, config, extra_dir=config.get('hyperopt_path')) + hyperoptloss_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']) if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): raise OperationalException( - f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.") + f"Found hyperopt {hyperoptloss_name} does not implement `hyperopt_loss_function`.") def _load_hyperoptloss( self, hyper_loss_name: str, config: Dict, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 5ff11d5ea..cf211e35b 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -25,7 +25,11 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, @pytest.fixture(scope='function') def hyperopt(default_conf, mocker): - default_conf.update({'spaces': ['all']}) + default_conf.update({ + 'spaces': ['all'], + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', + }) patch_exchange(mocker) return Hyperopt(default_conf) @@ -70,6 +74,8 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca args = [ 'hyperopt', '--config', 'config.json', + '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt-loss', 'DefaultHyperOptLoss', ] config = setup_configuration(get_args(args), RunMode.HYPEROPT) @@ -101,6 +107,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo args = [ 'hyperopt', '--config', 'config.json', + '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt-loss', 'DefaultHyperOptLoss', '--datadir', '/foo/bar', '--ticker-interval', '1m', '--timerange', ':100', @@ -155,7 +163,9 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt', MagicMock(return_value=hyperopts(default_conf)) ) - x = HyperOptResolver(default_conf, ).hyperopt + default_conf.update({'hyperopt': 'DefaultHyperOpts'}) + default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) + x = HyperOptResolver(default_conf).hyperopt assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_sell_trend') assert log_has("Hyperopt class does not provide populate_sell_trend() method. " @@ -169,7 +179,15 @@ def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None: default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): - HyperOptResolver(default_conf, ).hyperopt + HyperOptResolver(default_conf).hyperopt + + +def test_hyperoptresolver_noname(default_conf): + default_conf['hyperopt'] = '' + with pytest.raises(OperationalException, + match="No Hyperopt set. Please use `--customhyperopt` to specify " + "the Hyperopt class to use."): + HyperOptResolver(default_conf) def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: @@ -179,7 +197,8 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', MagicMock(return_value=hl) ) - x = HyperOptLossResolver(default_conf, ).hyperoptloss + default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) + x = HyperOptLossResolver(default_conf).hyperoptloss assert hasattr(x, "hyperopt_loss_function") @@ -187,7 +206,17 @@ def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None: default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): - HyperOptLossResolver(default_conf, ).hyperopt + HyperOptLossResolver(default_conf).hyperopt + + +def test_hyperoptlossresolver_noname(default_conf): + default_conf.update({'hyperopt': 'DefaultHyperOpts'}) + default_conf['hyperopt_loss'] = '' + with pytest.raises(OperationalException, + match="No Hyperopt Loss Function set. Please use " + "`--hyperopt-loss` to specify " + "the Hyperopt Loss Function class to use."): + HyperOptLossResolver(default_conf) def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None: @@ -200,6 +229,8 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None args = [ 'hyperopt', '--config', 'config.json', + '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] args = get_args(args) @@ -217,6 +248,8 @@ def test_start(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', + '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] args = get_args(args) @@ -239,6 +272,8 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', + '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] args = get_args(args) @@ -256,6 +291,8 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', + '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] args = get_args(args) @@ -264,6 +301,7 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: + default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) hl = HyperOptLossResolver(default_conf).hyperoptloss correct = hl.hyperopt_loss_function(hyperopt_results, 600) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100) @@ -276,6 +314,7 @@ def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) resultsb = hyperopt_results.copy() resultsb.loc[1, 'trade_duration'] = 20 + default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) hl = HyperOptLossResolver(default_conf).hyperoptloss longer = hl.hyperopt_loss_function(hyperopt_results, 100) shorter = hl.hyperopt_loss_function(resultsb, 100) @@ -288,6 +327,7 @@ def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) hl = HyperOptLossResolver(default_conf).hyperoptloss correct = hl.hyperopt_loss_function(hyperopt_results, 600) over = hl.hyperopt_loss_function(results_over, 600) @@ -407,6 +447,8 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -510,10 +552,13 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None: def test_generate_optimizer(mocker, default_conf) -> None: - default_conf.update({'config': 'config.json.example'}) - default_conf.update({'timerange': None}) - default_conf.update({'spaces': 'all'}) - default_conf.update({'hyperopt_min_trades': 1}) + default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', + 'timerange': None, + 'spaces': 'all', + 'hyperopt_min_trades': 1, + }) trades = [ ('POWR/BTC', 0.023117, 0.000233, 100) @@ -576,6 +621,8 @@ def test_generate_optimizer(mocker, default_conf) -> None: def test_clean_hyperopt(mocker, default_conf, caplog): patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -592,6 +639,8 @@ def test_clean_hyperopt(mocker, default_conf, caplog): def test_continue_hyperopt(mocker, default_conf, caplog): patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -621,6 +670,8 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -658,6 +709,8 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'roi stoploss', @@ -696,6 +749,8 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'roi stoploss', @@ -737,6 +792,8 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -770,6 +827,8 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'buy', @@ -815,6 +874,8 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'sell', @@ -862,6 +923,8 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho patch_exchange(mocker) default_conf.update({'config': 'config.json.example', + 'hyperopt': 'DefaultHyperOpts', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': space, From 08e6d8a7809222c217ba7050a9114dbab385d1f2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 11 Oct 2019 23:33:22 +0300 Subject: [PATCH 026/319] Rollback defaulting to DefaultHyperOptLoss --- .travis.yml | 2 +- docs/bot-usage.md | 3 ++- freqtrade/configuration/cli_options.py | 4 ++- freqtrade/constants.py | 1 + freqtrade/resolvers/hyperopt_resolver.py | 10 +++---- tests/optimize/test_hyperopt.py | 33 ------------------------ 6 files changed, 11 insertions(+), 42 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7a75a76c2..a45334dd6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ jobs: name: backtest - script: - cp config.json.example config.json - - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy --customhyperopt DefaultHyperOpts --hyperopt-loss DefaultHyperOptLoss + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy --customhyperopt DefaultHyperOpts name: hyperopt - script: flake8 name: flake8 diff --git a/docs/bot-usage.md b/docs/bot-usage.md index fcf82826a..8f7e0bbcf 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -322,7 +322,8 @@ optional arguments: generate completely different results, since the target for optimization is different. Built-in Hyperopt-loss-functions are: DefaultHyperOptLoss, - OnlyProfitHyperOptLoss, SharpeHyperOptLoss. + OnlyProfitHyperOptLoss, SharpeHyperOptLoss (default: + `DefaultHyperOptLoss`). Common arguments: diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 6928ddfdb..ee0d94023 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -231,8 +231,10 @@ AVAILABLE_CLI_OPTIONS = { 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. Built-in Hyperopt-loss-functions are: ' - 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.', + 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss ' + '(default: `%(default)s`).', metavar='NAME', + default=constants.DEFAULT_HYPEROPT_LOSS, ), # List exchanges "print_one_column": Arg( diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 2f490c900..b053519b0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -9,6 +9,7 @@ PROCESS_THROTTLE_SECS = 5 # sec DEFAULT_TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec +DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' UNLIMITED_STAKE_AMOUNT = 'unlimited' diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 45fe2548e..d1bc90e13 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Optional, Dict from freqtrade import OperationalException +from freqtrade.constants import DEFAULT_HYPEROPT_LOSS from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver @@ -87,12 +88,9 @@ class HyperOptLossResolver(IResolver): """ config = config or {} - if not config.get('hyperopt_loss'): - raise OperationalException("No Hyperopt Loss Function set. Please use " - "`--hyperopt-loss` to specify " - "the Hyperopt Loss Function class to use.") - - hyperoptloss_name = config['hyperopt_loss'] + # Verify the hyperopt_loss is in the configuration, otherwise fallback to the + # default hyperopt loss + hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS self.hyperoptloss = self._load_hyperoptloss( hyperoptloss_name, config, extra_dir=config.get('hyperopt_path')) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index cf211e35b..e1ee649c8 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -28,7 +28,6 @@ def hyperopt(default_conf, mocker): default_conf.update({ 'spaces': ['all'], 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', }) patch_exchange(mocker) return Hyperopt(default_conf) @@ -75,7 +74,6 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca 'hyperopt', '--config', 'config.json', '--customhyperopt', 'DefaultHyperOpts', - '--hyperopt-loss', 'DefaultHyperOptLoss', ] config = setup_configuration(get_args(args), RunMode.HYPEROPT) @@ -108,7 +106,6 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo 'hyperopt', '--config', 'config.json', '--customhyperopt', 'DefaultHyperOpts', - '--hyperopt-loss', 'DefaultHyperOptLoss', '--datadir', '/foo/bar', '--ticker-interval', '1m', '--timerange', ':100', @@ -164,7 +161,6 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: MagicMock(return_value=hyperopts(default_conf)) ) default_conf.update({'hyperopt': 'DefaultHyperOpts'}) - default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) x = HyperOptResolver(default_conf).hyperopt assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_sell_trend') @@ -197,7 +193,6 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', MagicMock(return_value=hl) ) - default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) x = HyperOptLossResolver(default_conf).hyperoptloss assert hasattr(x, "hyperopt_loss_function") @@ -209,16 +204,6 @@ def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None: HyperOptLossResolver(default_conf).hyperopt -def test_hyperoptlossresolver_noname(default_conf): - default_conf.update({'hyperopt': 'DefaultHyperOpts'}) - default_conf['hyperopt_loss'] = '' - with pytest.raises(OperationalException, - match="No Hyperopt Loss Function set. Please use " - "`--hyperopt-loss` to specify " - "the Hyperopt Loss Function class to use."): - HyperOptLossResolver(default_conf) - - def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, default_conf) @@ -230,7 +215,6 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None 'hyperopt', '--config', 'config.json', '--customhyperopt', 'DefaultHyperOpts', - '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] args = get_args(args) @@ -249,7 +233,6 @@ def test_start(mocker, default_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--customhyperopt', 'DefaultHyperOpts', - '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] args = get_args(args) @@ -273,7 +256,6 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--customhyperopt', 'DefaultHyperOpts', - '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] args = get_args(args) @@ -292,7 +274,6 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--customhyperopt', 'DefaultHyperOpts', - '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] args = get_args(args) @@ -301,7 +282,6 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: - default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) hl = HyperOptLossResolver(default_conf).hyperoptloss correct = hl.hyperopt_loss_function(hyperopt_results, 600) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100) @@ -314,7 +294,6 @@ def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) resultsb = hyperopt_results.copy() resultsb.loc[1, 'trade_duration'] = 20 - default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) hl = HyperOptLossResolver(default_conf).hyperoptloss longer = hl.hyperopt_loss_function(hyperopt_results, 100) shorter = hl.hyperopt_loss_function(resultsb, 100) @@ -327,7 +306,6 @@ def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - default_conf.update({'hyperopt_loss': 'DefaultHyperOptLoss'}) hl = HyperOptLossResolver(default_conf).hyperoptloss correct = hl.hyperopt_loss_function(hyperopt_results, 600) over = hl.hyperopt_loss_function(results_over, 600) @@ -448,7 +426,6 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -554,7 +531,6 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None: def test_generate_optimizer(mocker, default_conf) -> None: default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'timerange': None, 'spaces': 'all', 'hyperopt_min_trades': 1, @@ -622,7 +598,6 @@ def test_clean_hyperopt(mocker, default_conf, caplog): patch_exchange(mocker) default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -640,7 +615,6 @@ def test_continue_hyperopt(mocker, default_conf, caplog): patch_exchange(mocker) default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -671,7 +645,6 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -710,7 +683,6 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'roi stoploss', @@ -750,7 +722,6 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'roi stoploss', @@ -793,7 +764,6 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -828,7 +798,6 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'buy', @@ -875,7 +844,6 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': 'sell', @@ -924,7 +892,6 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpts', - 'hyperopt_loss': 'DefaultHyperOptLoss', 'epochs': 1, 'timerange': None, 'spaces': space, From ff1fa17dc3243cc3a9c1469ef92b542c49e86d47 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 13 Oct 2019 03:41:25 +0300 Subject: [PATCH 027/319] No default value for the config parameter --- freqtrade/resolvers/hyperopt_resolver.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index d1bc90e13..15080cda5 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -22,13 +22,11 @@ class HyperOptResolver(IResolver): """ __slots__ = ['hyperopt'] - def __init__(self, config: Dict = None) -> None: + def __init__(self, config: Dict) -> None: """ Load the custom class from config parameter :param config: configuration dictionary """ - config = config or {} - if not config.get('hyperopt'): raise OperationalException("No Hyperopt set. Please use `--customhyperopt` to specify " "the Hyperopt class to use.") @@ -81,12 +79,11 @@ class HyperOptLossResolver(IResolver): """ __slots__ = ['hyperoptloss'] - def __init__(self, config: Dict = None) -> None: + def __init__(self, config: Dict) -> None: """ Load the custom class from config parameter - :param config: configuration dictionary or None + :param config: configuration dictionary """ - config = config or {} # Verify the hyperopt_loss is in the configuration, otherwise fallback to the # default hyperopt loss @@ -100,7 +97,8 @@ class HyperOptLossResolver(IResolver): if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): raise OperationalException( - f"Found hyperopt {hyperoptloss_name} does not implement `hyperopt_loss_function`.") + f"Found HyperoptLoss class {hyperoptloss_name} does not " + "implement `hyperopt_loss_function`.") def _load_hyperoptloss( self, hyper_loss_name: str, config: Dict, From 89283ef486ad48a03f0ee694a1784be1b279702b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Oct 2019 19:42:28 +0200 Subject: [PATCH 028/319] Rename --custom-hyperopt to --hyperopt --- .travis.yml | 2 +- docs/bot-usage.md | 4 ++-- docs/hyperopt.md | 2 +- freqtrade/configuration/cli_options.py | 2 +- freqtrade/resolvers/hyperopt_resolver.py | 2 +- tests/optimize/test_hyperopt.py | 14 +++++++------- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index a45334dd6..14466d2c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ jobs: name: backtest - script: - cp config.json.example config.json - - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy --customhyperopt DefaultHyperOpts + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy --hyperopt DefaultHyperOpts name: hyperopt - script: flake8 name: flake8 diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8f7e0bbcf..cf59bc11f 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -266,7 +266,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] - [--customhyperopt NAME] [--hyperopt-path PATH] + [--hyperopt NAME] [--hyperopt-path PATH] [--eps] [-e INT] [--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--dmmp] [--print-all] [--no-color] [--print-json] @@ -284,7 +284,7 @@ optional arguments: Specify max_open_trades to use. --stake_amount STAKE_AMOUNT Specify stake_amount. - --customhyperopt NAME + --hyperopt NAME Specify hyperopt class name which will be used by the bot. --hyperopt-path PATH Specify additional lookup path for Hyperopts and diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e6f753072..66c250eb7 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -239,7 +239,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade hyperopt --config config.json --customhyperopt -e 5000 --spaces all +freqtrade hyperopt --config config.json --hyperopt -e 5000 --spaces all ``` Use `` as the name of the custom hyperopt used. diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index ee0d94023..3a4629ada 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -152,7 +152,7 @@ AVAILABLE_CLI_OPTIONS = { ), # Hyperopt "hyperopt": Arg( - '--customhyperopt', + '--hyperopt', help='Specify hyperopt class name which will be used by the bot.', metavar='NAME', ), diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 15080cda5..a51935500 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -28,7 +28,7 @@ class HyperOptResolver(IResolver): :param config: configuration dictionary """ if not config.get('hyperopt'): - raise OperationalException("No Hyperopt set. Please use `--customhyperopt` to specify " + raise OperationalException("No Hyperopt set. Please use `--hyperopt` to specify " "the Hyperopt class to use.") hyperopt_name = config['hyperopt'] diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index e1ee649c8..1c89bb37c 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -73,7 +73,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca args = [ 'hyperopt', '--config', 'config.json', - '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpts', ] config = setup_configuration(get_args(args), RunMode.HYPEROPT) @@ -105,7 +105,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo args = [ 'hyperopt', '--config', 'config.json', - '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpts', '--datadir', '/foo/bar', '--ticker-interval', '1m', '--timerange', ':100', @@ -181,7 +181,7 @@ def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None: def test_hyperoptresolver_noname(default_conf): default_conf['hyperopt'] = '' with pytest.raises(OperationalException, - match="No Hyperopt set. Please use `--customhyperopt` to specify " + match="No Hyperopt set. Please use `--hyperopt` to specify " "the Hyperopt class to use."): HyperOptResolver(default_conf) @@ -214,7 +214,7 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None args = [ 'hyperopt', '--config', 'config.json', - '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpts', '--epochs', '5' ] args = get_args(args) @@ -232,7 +232,7 @@ def test_start(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', - '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpts', '--epochs', '5' ] args = get_args(args) @@ -255,7 +255,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', - '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpts', '--epochs', '5' ] args = get_args(args) @@ -273,7 +273,7 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', - '--customhyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpts', '--epochs', '5' ] args = get_args(args) From a5c83b66df0274e126be59d997d5aae210589159 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Oct 2019 06:51:03 +0200 Subject: [PATCH 029/319] Add --dry-run to trade command --- docs/bot-usage.md | 5 +++-- freqtrade/configuration/arguments.py | 2 +- freqtrade/configuration/cli_options.py | 5 +++++ freqtrade/configuration/configuration.py | 4 ++++ tests/test_configuration.py | 17 +++++++++++++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8f7e0bbcf..a258ee7f5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -36,7 +36,7 @@ optional arguments: ``` usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] - [--db-url PATH] [--sd-notify] + [--db-url PATH] [--sd-notify] [--dry-run] optional arguments: -h, --help show this help message and exit @@ -44,6 +44,8 @@ optional arguments: deployments (default: `sqlite:///tradesv3.sqlite` for Live Run mode, `sqlite://` for Dry Run). --sd-notify Notify systemd service manager. + --dry-run Enforce dry-run for trading, removes API keys and + simulates trades. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). @@ -63,7 +65,6 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. - ``` ### How to specify which configuration file be used? diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index a8d4b48f1..5735599df 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -12,7 +12,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_TRADE = ["db_url", "sd_notify"] +ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index ee0d94023..400d08e37 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -86,6 +86,11 @@ AVAILABLE_CLI_OPTIONS = { help='Notify systemd service manager.', action='store_true', ), + "dry_run": Arg( + '--dry-run', + help='Enforce dry-run for trading, removes API keys and simulates trades.', + action='store_true', + ), # Optimize common "ticker_interval": Arg( '-i', '--ticker-interval', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index ac27a5c99..7e992d6dd 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -162,6 +162,10 @@ class Configuration: if 'sd_notify' in self.args and self.args["sd_notify"]: config['internals'].update({'sd_notify': True}) + self._args_to_config(config, argname='dry_run', + logstring='Parameter --dry-run detected, ' + 'overriding dry_run to: {} ...') + def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load directory configurations diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 333a8992a..67c97a0bf 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -304,6 +304,23 @@ def test_load_config_with_params(default_conf, mocker) -> None: assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL +@pytest.mark.parametrize("config_value,expected,arglist", [ + (True, True, ['trade', '--dry-run']), # Leave config untouched + (False, True, ['trade', '--dry-run']), # Override config untouched + (False, False, ['trade']), # Leave config untouched + (True, True, ['trade']), # Leave config untouched +]) +def test_load_dry_run(default_conf, mocker, config_value, expected, arglist) -> None: + + default_conf['dry_run'] = config_value + patched_configuration_load_config_file(mocker, default_conf) + + configuration = Configuration(Arguments(arglist).get_parsed_arg()) + validated_conf = configuration.load_config() + + assert validated_conf.get('dry_run') is expected + + def test_load_custom_strategy(default_conf, mocker) -> None: default_conf.update({ 'strategy': 'CustomStrategy', From 6fb96183c08342343604cb5c93296f812a4849b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Oct 2019 12:26:06 +0200 Subject: [PATCH 030/319] Reword help string --- docs/bot-usage.md | 4 ++-- freqtrade/configuration/cli_options.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index a258ee7f5..9af960858 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -44,8 +44,8 @@ optional arguments: deployments (default: `sqlite:///tradesv3.sqlite` for Live Run mode, `sqlite://` for Dry Run). --sd-notify Notify systemd service manager. - --dry-run Enforce dry-run for trading, removes API keys and - simulates trades. + --dry-run Enforce dry-run for trading (removes Exchange secrets + and simulates trades). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 400d08e37..b3dcd52c2 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -88,7 +88,7 @@ AVAILABLE_CLI_OPTIONS = { ), "dry_run": Arg( '--dry-run', - help='Enforce dry-run for trading, removes API keys and simulates trades.', + help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).', action='store_true', ), # Optimize common From 2d34c0f52de724e87f728ca1c749128eb3cfb253 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Oct 2019 19:35:38 +0200 Subject: [PATCH 031/319] Update helpstring exports --- docs/bot-usage.md | 47 +++++++++++++------------- freqtrade/configuration/cli_options.py | 2 +- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index d2b9757f5..8c85965a4 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -9,19 +9,21 @@ This page explains the different parameters of the bot and how to run it. ``` usage: freqtrade [-h] [-V] - {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,download-data,plot-dataframe,plot-profit} + {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit} ... Free, open source crypto trading bot positional arguments: - {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,download-data,plot-dataframe,plot-profit} + {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit} trade Trade module. backtesting Backtesting module. edge Edge module. hyperopt Hyperopt module. create-userdir Create user-data directory. list-exchanges Print available exchanges. + list-timeframes Print available ticker intervals (timeframes) for the + exchange. download-data Download backtesting data. plot-dataframe Plot candles with indicators. plot-profit Generate plot showing profits. @@ -29,6 +31,7 @@ positional arguments: optional arguments: -h, --help show this help message and exit -V, --version show program's version number and exit + ``` ### Bot trading commands @@ -190,7 +193,8 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] - [--stake_amount STAKE_AMOUNT] [--eps] [--dmmp] + [--stake_amount STAKE_AMOUNT] [--fee FLOAT] + [--eps] [--dmmp] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -225,11 +229,10 @@ optional arguments: --export EXPORT Export backtest results, argument are: trades. Example: `--export=trades` --export-filename PATH - Save backtest results to the file with this filename - (default: `user_data/backtest_results/backtest- - result.json`). Requires `--export` to be set as well. - Example: `--export-filename=user_data/backtest_results - /backtest_today.json` + Save backtest results to the file with this filename. + Requires `--export` to be set as well. Example: + `--export-filename=user_data/backtest_results/backtest + _today.json` Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). @@ -246,8 +249,8 @@ Common arguments: Strategy arguments: -s NAME, --strategy NAME - Specify strategy class name (default: - `DefaultStrategy`). + Specify strategy class name which will be used by the + bot. --strategy-path PATH Specify additional strategy lookup path. ``` @@ -268,9 +271,9 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] - [--stake_amount STAKE_AMOUNT] - [--hyperopt NAME] [--hyperopt-path PATH] - [--eps] [-e INT] + [--stake_amount STAKE_AMOUNT] [--fee FLOAT] + [--hyperopt NAME] [--hyperopt-path PATH] [--eps] + [-e INT] [--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--dmmp] [--print-all] [--no-color] [--print-json] [-j JOBS] [--random-state INT] [--min-trades INT] @@ -287,8 +290,9 @@ optional arguments: Specify max_open_trades to use. --stake_amount STAKE_AMOUNT Specify stake_amount. - --hyperopt NAME - Specify hyperopt class name which will be used by the + --fee FLOAT Specify fee ratio. Will be applied twice (on trade + entry and exit). + --hyperopt NAME Specify hyperopt class name which will be used by the bot. --hyperopt-path PATH Specify additional lookup path for Hyperopts and Hyperopt Loss functions. @@ -328,7 +332,6 @@ optional arguments: OnlyProfitHyperOptLoss, SharpeHyperOptLoss (default: `DefaultHyperOptLoss`). - Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). --logfile FILE Log to the file specified. @@ -344,10 +347,9 @@ Common arguments: Strategy arguments: -s NAME, --strategy NAME - Specify strategy class name (default: - `DefaultStrategy`). + Specify strategy class name which will be used by the + bot. --strategy-path PATH Specify additional strategy lookup path. - ``` ## Edge commands @@ -359,7 +361,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] - [--stoplosses STOPLOSS_RANGE] + [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit @@ -395,10 +397,9 @@ Common arguments: Strategy arguments: -s NAME, --strategy NAME - Specify strategy class name (default: - `DefaultStrategy`). + Specify strategy class name which will be used by the + bot. --strategy-path PATH Specify additional strategy lookup path. - ``` To understand edge and how to read the results, please read the [edge documentation](edge.md). diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index a62de10eb..ac72cff0b 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -140,7 +140,7 @@ AVAILABLE_CLI_OPTIONS = { ), "exportfilename": Arg( '--export-filename', - help='Save backtest results to the file with this filename (default: `%(default)s`). ' + help='Save backtest results to the file with this filename. ' 'Requires `--export` to be set as well. ' 'Example: `--export-filename=user_data/backtest_results/backtest_today.json`', metavar='PATH', From 1c503f39b2226ae19133a7c03131cede6905f03b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Oct 2019 19:54:38 +0200 Subject: [PATCH 032/319] Handle some merge aftermaths --- .travis.yml | 2 +- freqtrade/configuration/arguments.py | 39 ++++++++++++++++------------ tests/optimize/test_hyperopt.py | 38 +++++++++++++-------------- tests/strategy/test_strategy.py | 2 +- tests/test_arguments.py | 4 +-- tests/test_utils.py | 4 +-- 6 files changed, 47 insertions(+), 42 deletions(-) diff --git a/.travis.yml b/.travis.yml index 14466d2c4..d5d4ad8c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ jobs: name: backtest - script: - cp config.json.example config.json - - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy --hyperopt DefaultHyperOpts + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy --hyperopt DefaultHyperOpt name: hyperopt - script: flake8 name: flake8 diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index b093a5015..bc58fc8e8 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -153,41 +153,46 @@ class Arguments: self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd) # Add list-exchanges subcommand - list_exchanges_cmd = subparsers.add_parser('list-exchanges', - help='Print available exchanges.', - parents=[_common_parser], - ) + list_exchanges_cmd = subparsers.add_parser( + 'list-exchanges', + help='Print available exchanges.', + parents=[_common_parser], + ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) # Add list-timeframes subcommand list_timeframes_cmd = subparsers.add_parser( 'list-timeframes', - help='Print available ticker intervals (timeframes) for the exchange.' + help='Print available ticker intervals (timeframes) for the exchange.', + parents=[_common_parser], ) list_timeframes_cmd.set_defaults(func=start_list_timeframes) self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd) # Add download-data subcommand - download_data_cmd = subparsers.add_parser('download-data', - help='Download backtesting data.', - parents=[_common_parser], - ) + download_data_cmd = subparsers.add_parser( + 'download-data', + help='Download backtesting data.', + parents=[_common_parser], + ) download_data_cmd.set_defaults(func=start_download_data) self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd) # Add Plotting subcommand - plot_dataframe_cmd = subparsers.add_parser('plot-dataframe', - help='Plot candles with indicators.', - parents=[_common_parser, _strategy_parser], - ) + plot_dataframe_cmd = subparsers.add_parser( + 'plot-dataframe', + help='Plot candles with indicators.', + parents=[_common_parser, _strategy_parser], + ) plot_dataframe_cmd.set_defaults(func=start_plot_dataframe) self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd) # Plot profit - plot_profit_cmd = subparsers.add_parser('plot-profit', - help='Generate plot showing profits.', - parents=[_common_parser], - ) + plot_profit_cmd = subparsers.add_parser( + 'plot-profit', + help='Generate plot showing profits.', + parents=[_common_parser], + ) plot_profit_cmd.set_defaults(func=start_plot_profit) self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c9ecb63dc..6bed0f8cc 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -27,7 +27,7 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, def hyperopt(default_conf, mocker): default_conf.update({ 'spaces': ['all'], - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', }) patch_exchange(mocker) return Hyperopt(default_conf) @@ -73,7 +73,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpt', ] config = setup_configuration(get_args(args), RunMode.HYPEROPT) @@ -105,7 +105,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpt', '--datadir', '/foo/bar', '--ticker-interval', '1m', '--timerange', ':100', @@ -160,7 +160,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt', MagicMock(return_value=hyperopt(default_conf)) ) - default_conf.update({'hyperopt': 'DefaultHyperOpts'}) + default_conf.update({'hyperopt': 'DefaultHyperOpt'}) x = HyperOptResolver(default_conf).hyperopt assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_sell_trend') @@ -214,7 +214,7 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpt', '--epochs', '5' ] args = get_args(args) @@ -232,7 +232,7 @@ def test_start(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpt', '--epochs', '5' ] args = get_args(args) @@ -255,7 +255,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpt', '--epochs', '5' ] args = get_args(args) @@ -273,7 +273,7 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'DefaultHyperOpts', + '--hyperopt', 'DefaultHyperOpt', '--epochs', '5' ] args = get_args(args) @@ -425,7 +425,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -530,7 +530,7 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None: def test_generate_optimizer(mocker, default_conf) -> None: default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'timerange': None, 'spaces': 'all', 'hyperopt_min_trades': 1, @@ -597,7 +597,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: def test_clean_hyperopt(mocker, default_conf, caplog): patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -614,7 +614,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog): def test_continue_hyperopt(mocker, default_conf, caplog): patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -644,7 +644,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -682,7 +682,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'roi stoploss', @@ -721,7 +721,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'roi stoploss', @@ -763,7 +763,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'all', @@ -797,7 +797,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'buy', @@ -843,7 +843,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': 'sell', @@ -891,7 +891,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho patch_exchange(mocker) default_conf.update({'config': 'config.json.example', - 'hyperopt': 'DefaultHyperOpts', + 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, 'timerange': None, 'spaces': space, diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 285ac1bbf..97affc99c 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -61,7 +61,7 @@ def test_load_strategy_invalid_directory(result, caplog, default_conf): 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'}) + assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_not_found_strategy(default_conf): diff --git a/tests/test_arguments.py b/tests/test_arguments.py index be711aeda..d8fbace0f 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -186,7 +186,7 @@ def test_config_notallowed(mocker) -> None: ] pargs = Arguments(args).get_parsed_arg() - assert pargs["config"] is None + assert "config" not in pargs # When file exists: mocker.patch.object(Path, "is_file", MagicMock(return_value=True)) @@ -195,7 +195,7 @@ def test_config_notallowed(mocker) -> None: ] pargs = Arguments(args).get_parsed_arg() # config is not added even if it exists, since create-userdir is in the notallowed list - assert pargs["config"] is None + assert "config" not in pargs def test_config_notrequired(mocker) -> None: diff --git a/tests/test_utils.py b/tests/test_utils.py index f798dcfb8..4e76bb6ca 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -95,8 +95,8 @@ def test_list_timeframes(mocker, capsys): # Test with --config config.json.example args = [ - '--config', 'config.json.example', "list-timeframes", + '--config', 'config.json.example', ] start_list_timeframes(get_args(args)) captured = capsys.readouterr() @@ -139,8 +139,8 @@ def test_list_timeframes(mocker, capsys): # Test with --one-column args = [ - '--config', 'config.json.example', "list-timeframes", + '--config', 'config.json.example', "--one-column", ] start_list_timeframes(get_args(args)) From e1edf363076b9b6397e0f2d78a7323f922fb5989 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Oct 2019 06:22:05 +0200 Subject: [PATCH 033/319] Fix test failures --- freqtrade/configuration/arguments.py | 6 +++-- tests/test_utils.py | 34 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index e62921af8..29d0d98a2 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -179,7 +179,8 @@ class Arguments: # Add list-markets subcommand list_markets_cmd = subparsers.add_parser( 'list-markets', - help='Print markets on exchange.' + help='Print markets on exchange.', + parents=[_common_parser], ) list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False)) self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd) @@ -187,7 +188,8 @@ class Arguments: # Add list-pairs subcommand list_pairs_cmd = subparsers.add_parser( 'list-pairs', - help='Print pairs on exchange.' + help='Print pairs on exchange.', + parents=[_common_parser], ) list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True)) self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd) diff --git a/tests/test_utils.py b/tests/test_utils.py index c598cfd76..8cd9c3aef 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -182,8 +182,8 @@ def test_list_markets(mocker, markets, capsys): # Test with --config config.json.example args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--print-list", ] start_list_markets(get_args(args), False) @@ -208,8 +208,8 @@ def test_list_markets(mocker, markets, capsys): patch_exchange(mocker, api_mock=api_mock, id="bittrex") # Test with --all: all markets args = [ - '--config', 'config.json.example', "list-markets", "--all", + '--config', 'config.json.example', "--print-list", ] start_list_markets(get_args(args), False) @@ -221,8 +221,8 @@ def test_list_markets(mocker, markets, capsys): # Test list-pairs subcommand: active pairs args = [ - '--config', 'config.json.example', "list-pairs", + '--config', 'config.json.example', "--print-list", ] start_list_markets(get_args(args), True) @@ -233,8 +233,8 @@ def test_list_markets(mocker, markets, capsys): # Test list-pairs subcommand with --all: all pairs args = [ - '--config', 'config.json.example', "list-pairs", "--all", + '--config', 'config.json.example', "--print-list", ] start_list_markets(get_args(args), True) @@ -246,8 +246,8 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=ETH, LTC args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "ETH", "LTC", "--print-list", ] @@ -259,8 +259,8 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=LTC args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--print-list", ] @@ -272,8 +272,8 @@ def test_list_markets(mocker, markets, capsys): # active markets, quote=USDT, USD args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--quote", "USDT", "USD", "--print-list", ] @@ -285,8 +285,8 @@ def test_list_markets(mocker, markets, capsys): # active markets, quote=USDT args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--quote", "USDT", "--print-list", ] @@ -298,8 +298,8 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=LTC, quote=USDT args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--quote", "USDT", "--print-list", ] @@ -311,8 +311,8 @@ def test_list_markets(mocker, markets, capsys): # active pairs, base=LTC, quote=USDT args = [ - '--config', 'config.json.example', "list-pairs", + '--config', 'config.json.example', "--base", "LTC", "--quote", "USDT", "--print-list", ] @@ -324,8 +324,8 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=LTC, quote=USDT, NONEXISTENT args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--quote", "USDT", "NONEXISTENT", "--print-list", ] @@ -337,8 +337,8 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=LTC, quote=NONEXISTENT args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--quote", "NONEXISTENT", "--print-list", ] @@ -350,8 +350,8 @@ def test_list_markets(mocker, markets, capsys): # Test tabular output args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', ] start_list_markets(get_args(args), False) captured = capsys.readouterr() @@ -360,8 +360,8 @@ def test_list_markets(mocker, markets, capsys): # Test tabular output, no markets found args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--quote", "NONEXISTENT", ] start_list_markets(get_args(args), False) @@ -372,8 +372,8 @@ def test_list_markets(mocker, markets, capsys): # Test --print-json args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--print-json" ] start_list_markets(get_args(args), False) @@ -383,8 +383,8 @@ def test_list_markets(mocker, markets, capsys): # Test --print-csv args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--print-csv" ] start_list_markets(get_args(args), False) @@ -395,8 +395,8 @@ def test_list_markets(mocker, markets, capsys): # Test --one-column args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--one-column" ] start_list_markets(get_args(args), False) From 13255b370c270a556f2e5883de0ac88abc801667 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Oct 2019 06:30:07 +0200 Subject: [PATCH 034/319] Allow non-config to parse 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 7bb49dc31..393fd78be 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -93,7 +93,7 @@ class Configuration: :return: Configuration dictionary """ # Load all configs - config: Dict[str, Any] = self.load_from_files(self.args["config"]) + config: Dict[str, Any] = self.load_from_files(self.args.get("config", [])) # Keep a copy of the original configuration file config['original_config'] = deepcopy(config) From b3e028e853789f64754a8ee054a0b45b0a65c371 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Oct 2019 06:43:52 +0200 Subject: [PATCH 035/319] Improve dynamic pairlist documentation --- docs/configuration.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 0eff4da88..dd54b6c54 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -417,10 +417,15 @@ section of the configuration. `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. * There is a possibility to filter low-value coins that would not allow setting a stop loss (set `precision_filter` parameter to `true` for this). + * `VolumePairList` does not consider `pair_whitelist`, but builds this automatically based the pairlist configuration. Example: ```json +"exchange": { + "pair_whitelist": [], + "pair_blacklist": ["BNB/BTC"] +}, "pairlist": { "method": "VolumePairList", "config": { From 45b83cc5441e4bf08fc30ee4379ebe5a71a0a32d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Oct 2019 07:07:01 +0200 Subject: [PATCH 036/319] Don't require pair_whitelist for dynamicPairlist usecases --- freqtrade/configuration/config_validation.py | 15 ++++++++++- freqtrade/configuration/configuration.py | 3 +++ freqtrade/constants.py | 2 +- tests/test_configuration.py | 26 +++++++++++++++++++- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 6a8374e6d..93d93263f 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -5,7 +5,7 @@ from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import constants, OperationalException - +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -64,6 +64,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: # validating trailing stoploss _validate_trailing_stoploss(conf) _validate_edge(conf) + _validate_whitelist(conf) def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: @@ -111,3 +112,15 @@ def _validate_edge(conf: Dict[str, Any]) -> None: "Edge and VolumePairList are incompatible, " "Edge will override whatever pairs VolumePairlist selects." ) + + +def _validate_whitelist(conf: Dict[str, Any]) -> None: + """ + Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does. + """ + if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT]: + return + + if (conf.get('pairlist', {}).get('method', 'StaticPairList') == 'StaticPairList' + and not conf.get('exchange', {}).get('pair_whitelist')): + raise OperationalException("StaticPairList requires pair_whitelist to be set.") diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 034f8d386..be1c7ab4e 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -179,6 +179,9 @@ class Configuration: config['exchange']['name'] = self.args["exchange"] logger.info(f"Using exchange {config['exchange']['name']}") + if 'pair_whitelist' not in config['exchange']: + config['exchange']['pair_whitelist'] = [] + if 'user_data_dir' in self.args and self.args["user_data_dir"]: config.update({'user_data_dir': self.args["user_data_dir"]}) elif 'user_data_dir' not in config: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e8f3f5783..5fdd45916 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -235,7 +235,7 @@ CONF_SCHEMA = { 'ccxt_config': {'type': 'object'}, 'ccxt_async_config': {'type': 'object'} }, - 'required': ['name', 'pair_whitelist'] + 'required': ['name'] }, 'edge': { 'type': 'object', diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 2aa805fe6..cfb7f9a7f 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -729,6 +729,30 @@ def test_validate_edge(edge_conf): validate_config_consistency(edge_conf) +def test_validate_whitelist(default_conf): + default_conf['runmode'] = RunMode.DRY_RUN + # Test regular case - has whitelist and uses StaticPairlist + validate_config_consistency(default_conf) + conf = deepcopy(default_conf) + del conf['exchange']['pair_whitelist'] + # Test error case + with pytest.raises(OperationalException, + match="StaticPairList requires pair_whitelist to be set."): + + validate_config_consistency(conf) + + conf = deepcopy(default_conf) + + conf.update({"pairlist": { + "method": "VolumePairList", + }}) + # Dynamic whitelist should not care about pair_whitelist + validate_config_consistency(conf) + del conf['exchange']['pair_whitelist'] + + validate_config_consistency(conf) + + def test_load_config_test_comments() -> None: """ Load config with comments @@ -895,7 +919,7 @@ def test_pairlist_resolving_fallback(mocker): # Fix flaky tests if config.json exists args["config"] = None - configuration = Configuration(args) + configuration = Configuration(args, RunMode.OTHER) config = configuration.get_config() assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] From e63377980e7f0aa6f2937753eb6033518a6aaf53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Oct 2019 07:12:50 +0200 Subject: [PATCH 037/319] Improve pairlist documentation --- docs/configuration.md | 5 +++-- tests/test_configuration.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index dd54b6c54..33c296a6a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -75,8 +75,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** | `exchange.password` | '' | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). -| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). +| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used when using VolumePairList (see [below](#dynamic-pairlists)). +| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. @@ -418,6 +418,7 @@ section of the configuration. * There is a possibility to filter low-value coins that would not allow setting a stop loss (set `precision_filter` parameter to `true` for this). * `VolumePairList` does not consider `pair_whitelist`, but builds this automatically based the pairlist configuration. + * Pairs in `pair_blacklist` are not considered for VolumePairList, even if all other filters would match. Example: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index cfb7f9a7f..545dd5df4 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -825,7 +825,7 @@ def test_pairlist_resolving(): args = Arguments(arglist).get_parsed_arg() - configuration = Configuration(args) + configuration = Configuration(args, RunMode.OTHER) config = configuration.get_config() assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] From 2e896462c14b66e226b2a5e4afb054f99c229fbe Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Oct 2019 19:49:23 +0200 Subject: [PATCH 038/319] Fix wrong volumepairlist message --- freqtrade/pairlist/VolumePairList.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index b9b7977ab..5f53cd17b 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -54,7 +54,7 @@ class VolumePairList(IPairList): """ # Generate dynamic whitelist self._whitelist = self._gen_pair_whitelist( - self._config['stake_currency'], self._sort_key)[:self._number_pairs] + self._config['stake_currency'], self._sort_key) @cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: @@ -91,6 +91,6 @@ class VolumePairList(IPairList): valid_tickers.remove(t) pairs = [s['symbol'] for s in valid_tickers] - logger.info(f"Searching pairs: {self._whitelist}") + logger.info(f"Searching pairs: {pairs[:self._number_pairs]}") return pairs From d0521d33cedd079cff559a75571cb4cf2f9eef1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Oct 2019 09:38:29 +0200 Subject: [PATCH 039/319] Refactor whitelist handling fixes #2413 --- freqtrade/freqtradebot.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6a1be16a1..f6f12f7f8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -72,7 +72,7 @@ class FreqtradeBot: self.edge = Edge(self.config, self.exchange, self.strategy) if \ self.config.get('edge', {}).get('enabled', False) else None - self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] + self.active_pair_whitelist = self._refresh_whitelist() persistence.init(self.config.get('db_url', None), clean_open_orders=self.config.get('dry_run', False)) @@ -118,21 +118,10 @@ class FreqtradeBot: # Check whether markets have to be reloaded self.exchange._reload_markets() - # Refresh whitelist - self.pairlists.refresh_pairlist() - self.active_pair_whitelist = self.pairlists.whitelist - - # Calculating Edge positioning - if self.edge: - self.edge.calculate() - self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) - # Query trades from persistence layer trades = Trade.get_open_trades() - # Extend active-pair whitelist with pairs from open trades - # It ensures that tickers are downloaded for open trades - self._extend_whitelist_with_trades(self.active_pair_whitelist, trades) + self.active_pair_whitelist = self._refresh_whitelist(trades) # Refreshing candles self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), @@ -150,11 +139,24 @@ class FreqtradeBot: self.check_handle_timedout() Trade.session.flush() - def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): + def _refresh_whitelist(self, trades: List[Trade] = []) -> List[str]: """ - Extend whitelist with pairs from open trades + Refresh whitelist from pairlist or edge and extend it with trades. """ - whitelist.extend([trade.pair for trade in trades if trade.pair not in whitelist]) + # Refresh whitelist + self.pairlists.refresh_pairlist() + _whitelist = self.pairlists.whitelist + + # Calculating Edge positioning + if self.edge: + self.edge.calculate() + _whitelist = self.edge.adjust(_whitelist) + + if trades: + # Extend active-pair whitelist with pairs from open trades + # It ensures that tickers are downloaded for open trades + _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) + return _whitelist def _create_pair_whitelist(self, pairs: List[str]) -> List[Tuple[str, str]]: """ From f5351e60e7befbc44672f8f33f6d4076bc3dabf2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Oct 2019 10:08:23 +0200 Subject: [PATCH 040/319] Adjust markets mock --- tests/conftest.py | 29 ++++++++--- tests/exchange/test_exchange.py | 31 ++++------- tests/pairlist/test_pairlist.py | 16 +++--- tests/rpc/test_rpc.py | 91 +++++++++++---------------------- tests/test_freqtradebot.py | 49 ++++++++---------- 5 files changed, 93 insertions(+), 123 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 305221d6d..84612175d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,13 +55,15 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: +def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) + if mock_markets: + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) @@ -69,8 +71,8 @@ def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange: - patch_exchange(mocker, api_mock, id) +def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', mock_markets=True) -> Exchange: + patch_exchange(mocker, api_mock, id, mock_markets) config["exchange"]["name"] = id try: exchange = ExchangeResolver(id, config).exchange @@ -85,6 +87,11 @@ def patch_wallet(mocker, free=999.9) -> None: )) +def patch_whitelist(mocker, conf) -> None: + mocker.patch('freqtrade.freqtradebot.FreqtradeBot._refresh_whitelist', + MagicMock(return_value=conf['exchange']['pair_whitelist'])) + + def patch_edge(mocker) -> None: # "ETH/BTC", # "LTC/BTC", @@ -120,6 +127,8 @@ def patch_freqtradebot(mocker, config) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot._refresh_whitelist', + MagicMock(return_value=config['exchange']['pair_whitelist'])) def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: @@ -287,6 +296,10 @@ def ticker_sell_down(): @pytest.fixture def markets(): + return get_markets() + + +def get_markets(): return { 'ETH/BTC': { 'id': 'ethbtc', @@ -369,7 +382,7 @@ def markets(): 'symbol': 'LTC/BTC', 'base': 'LTC', 'quote': 'BTC', - 'active': False, + 'active': True, 'precision': { 'price': 8, 'amount': 8, @@ -394,7 +407,7 @@ def markets(): 'symbol': 'XRP/BTC', 'base': 'XRP', 'quote': 'BTC', - 'active': False, + 'active': True, 'precision': { 'price': 8, 'amount': 8, @@ -419,7 +432,7 @@ def markets(): 'symbol': 'NEO/BTC', 'base': 'NEO', 'quote': 'BTC', - 'active': False, + 'active': True, 'precision': { 'price': 8, 'amount': 8, @@ -444,7 +457,7 @@ def markets(): 'symbol': 'BTT/BTC', 'base': 'BTT', 'quote': 'BTC', - 'active': True, + 'active': False, 'precision': { 'base': 8, 'quote': 8, @@ -494,7 +507,7 @@ def markets(): 'symbol': 'LTC/USDT', 'base': 'LTC', 'quote': 'USDT', - 'active': True, + 'active': False, 'precision': { 'amount': 8, 'price': 8 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1e0a5fdc3..d3f50c6da 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -177,16 +177,11 @@ def test_symbol_amount_prec(default_conf, mocker): ''' Test rounds down to 4 Decimal places ''' - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' - }) - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance')) markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}}) - type(api_mock).markets = markets - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, id="binance") + mocker.patch('freqtrade.exchange.Exchange.markets', markets) amount = 2.34559 pair = 'ETH/BTC' @@ -198,16 +193,10 @@ def test_symbol_price_prec(default_conf, mocker): ''' Test rounds up to 4 decimal places ''' - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' - }) - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance')) - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}}) - type(api_mock).markets = markets - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, id="binance") + mocker.patch('freqtrade.exchange.Exchange.markets', markets) price = 2.34559 pair = 'ETH/BTC' @@ -279,7 +268,7 @@ def test__load_markets(default_conf, mocker, caplog): api_mock.load_markets = MagicMock(return_value=expected_return) type(api_mock).markets = expected_return default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] - ex = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + ex = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False) assert ex.markets == expected_return @@ -294,7 +283,8 @@ def test__reload_markets(default_conf, mocker, caplog): api_mock.load_markets = load_markets type(api_mock).markets = initial_markets default_conf['exchange']['markets_refresh_interval'] = 10 - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", + mock_markets=False) exchange._last_markets_refresh = arrow.utcnow().timestamp updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} @@ -1715,15 +1705,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), # active markets ([], [], False, True, - ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', - 'TKN/BTC', 'XLTCUSDT']), + ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'NEO/BTC', + 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), # all pairs ([], [], True, False, ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), # active pairs ([], [], True, True, - ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', 'TKN/BTC']), + ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'NEO/BTC', + 'TKN/BTC', 'XRP/BTC']), # all markets, base=ETH, LTC (['ETH', 'LTC'], [], False, False, ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 411ae60a3..929fc0ba0 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -80,7 +80,7 @@ def test_refresh_pairlist_dynamic(mocker, markets, tickers, whitelist_conf): freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) # argument: use the whitelist dynamically by exchange-volume - whitelist = ['ETH/BTC', 'TKN/BTC', 'BTT/BTC'] + whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'] freqtradebot.pairlists.refresh_pairlist() assert whitelist == freqtradebot.pairlists.whitelist @@ -108,12 +108,12 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): @pytest.mark.parametrize("precision_filter,base_currency,key,whitelist_result", [ - (False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'BTT/BTC']), - (False, "BTC", "bidVolume", ['BTT/BTC', 'TKN/BTC', 'ETH/BTC']), - (False, "USDT", "quoteVolume", ['ETH/USDT', 'LTC/USDT']), + (False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + (False, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC']), + (False, "USDT", "quoteVolume", ['ETH/USDT']), (False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange - (True, "BTC", "quoteVolume", ["ETH/BTC", "TKN/BTC"]), - (True, "BTC", "bidVolume", ["TKN/BTC", "ETH/BTC"]) + (True, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC"]), + (True, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC"]) ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers, base_currency, key, whitelist_result, precision_filter) -> None: @@ -127,7 +127,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers, freqtrade.pairlists._precision_filter = precision_filter freqtrade.config['stake_currency'] = base_currency whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key) - assert whitelist == whitelist_result + assert sorted(whitelist) == sorted(whitelist_result) def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: @@ -160,7 +160,7 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): (['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), # TRX/ETH wrong stake (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BCH/BTC not available (['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "is not compatible with exchange"), # BLK/BTC in blacklist - (['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], "Market is not active") # LTC/BTC is inactive + (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") # BTT/BTC is inactive ]) def test_validate_whitelist(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, log_message): diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 66468927f..a5da9b51e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -14,7 +14,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import patch_exchange, patch_get_signal +from tests.conftest import patch_exchange, patch_get_signal, get_patched_freqtradebot # Functions for recurrent object patching @@ -26,17 +26,15 @@ def prec_satoshi(a, b) -> float: # Unit tests -def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: +def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -98,17 +96,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: } == results[0] -def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: - patch_exchange(mocker) +def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -134,7 +130,6 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: def test_rpc_daily_profit(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -143,7 +138,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -181,22 +176,20 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, markets, mocker) -> None: + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -267,9 +260,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Test that rpc_trade_statistics can handle trades that lacks # trade.open_rate (it is set to None) -def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, +def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, ticker_sell_up, limit_buy_order, limit_sell_order): - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), @@ -281,10 +273,9 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -343,7 +334,6 @@ def test_rpc_balance_handle_error(default_conf, mocker): 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -352,7 +342,7 @@ def test_rpc_balance_handle_error(default_conf, mocker): get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx')) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() @@ -394,7 +384,6 @@ def test_rpc_balance_handle(default_conf, mocker): 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -406,7 +395,7 @@ def test_rpc_balance_handle(default_conf, mocker): side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}") ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() @@ -438,14 +427,13 @@ def test_rpc_balance_handle(default_conf, mocker): def test_rpc_start(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED @@ -460,14 +448,13 @@ def test_rpc_start(mocker, default_conf) -> None: def test_rpc_stop(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -483,14 +470,13 @@ def test_rpc_stop(mocker, default_conf) -> None: def test_rpc_stopbuy(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -501,8 +487,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: assert freqtradebot.config['max_open_trades'] == 0 -def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: - patch_exchange(mocker) +def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) cancel_order_mock = MagicMock() @@ -518,10 +503,9 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: } ), get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -606,18 +590,16 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: def test_performance_handle(default_conf, ticker, limit_buy_order, fee, - limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) + limit_sell_order, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -641,18 +623,16 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, assert prec_satoshi(res[0]['profit'], 6.2) -def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: - patch_exchange(mocker) +def test_rpc_count(mocker, default_conf, ticker, fee) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -665,9 +645,8 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: assert counts["current"] == 1 -def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None: +def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None: default_conf['forcebuy_enable'] = True - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( @@ -675,11 +654,10 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order get_balances=MagicMock(return_value=ticker), get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), buy=buy_mm ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -704,7 +682,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order # Test not buying default_conf['stake_amount'] = 0.0000001 - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'TKN/BTC' @@ -715,10 +693,9 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order def test_rpcforcebuy_stopped(mocker, default_conf) -> None: default_conf['forcebuy_enable'] = True default_conf['initial_state'] = 'stopped' - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -727,10 +704,9 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None: def test_rpcforcebuy_disabled(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -739,10 +715,9 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: def test_rpc_whitelist(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_whitelist() assert ret['method'] == 'StaticPairList' @@ -750,14 +725,13 @@ def test_rpc_whitelist(mocker, default_conf) -> None: def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: - patch_exchange(mocker) default_conf['pairlist'] = {'method': 'VolumePairList', 'config': {'number_assets': 4} } mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_whitelist() assert ret['method'] == 'VolumePairList' @@ -766,10 +740,9 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: def test_rpc_blacklist(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_blacklist(None) assert ret['method'] == 'StaticPairList' @@ -785,23 +758,21 @@ def test_rpc_blacklist(mocker, default_conf) -> None: def test_rpc_edge_disabled(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) with pytest.raises(RPCException, match=r'Edge is not enabled.'): rpc._rpc_edge() def test_rpc_edge_enabled(mocker, edge_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), } )) - freqtradebot = FreqtradeBot(edge_conf) + freqtradebot = get_patched_freqtradebot(mocker, edge_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_edge() diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f1533d867..ff9a34142 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -23,7 +23,7 @@ from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.worker import Worker from tests.conftest import (get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, - patch_get_signal, patch_wallet) + patch_get_signal, patch_wallet, patch_whitelist) def patch_RPCManager(mocker) -> MagicMock: @@ -1247,11 +1247,10 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, - markets, limit_buy_order, limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set stoploss_limit = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock(return_value={ @@ -1262,7 +1261,6 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, 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), stoploss_limit=stoploss_limit ) @@ -1272,7 +1270,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, # disabling ROI default_conf['minimal_roi']['0'] = 999999999 - freqtrade = FreqtradeBot(default_conf) + freqtrade = get_patched_freqtradebot(mocker, default_conf) # enabling stoploss on exchange freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1824,20 +1822,18 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, def test_handle_trade_roi(default_conf, ticker, limit_buy_order, - fee, mocker, markets, caplog) -> None: + fee, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtrade = FreqtradeBot(default_conf) + freqtrade = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtrade, value=(True, False)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) @@ -1858,20 +1854,18 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, def test_handle_trade_use_sell_signal( - default_conf, ticker, limit_buy_order, fee, mocker, markets, caplog) -> None: + default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None: # use_sell_signal is True buy default caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtrade = FreqtradeBot(default_conf) + freqtrade = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trades() @@ -2236,6 +2230,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc get_fee=fee, markets=PropertyMock(return_value=markets) ) + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2282,6 +2277,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, get_fee=fee, markets=PropertyMock(return_value=markets) ) + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2331,6 +2327,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe get_fee=fee, markets=PropertyMock(return_value=markets) ) + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2647,6 +2644,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, get_fee=fee, markets=PropertyMock(return_value=markets) ) + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2814,14 +2812,13 @@ 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: +def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2851,7 +2848,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mock 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: +def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2863,7 +2860,6 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'ignore_roi_if_buy_signal': True @@ -2885,7 +2881,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m assert trade.sell_reason == SellType.ROI.value -def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, mocker) -> None: +def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2897,9 +2893,9 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) default_conf['trailing_stop'] = True + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) @@ -2937,7 +2933,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets, +def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) @@ -2951,10 +2947,11 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0.01 + patch_whitelist(mocker, default_conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) @@ -2994,7 +2991,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, - caplog, mocker, markets) -> None: + caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) patch_exchange(mocker) @@ -3007,9 +3004,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) - + patch_whitelist(mocker, default_conf) default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0.01 default_conf['trailing_stop_positive_offset'] = 0.011 @@ -3054,7 +3050,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, - caplog, mocker, markets) -> None: + caplog, mocker) -> None: buy_price = limit_buy_order['price'] # buy_price: 0.00001099 @@ -3069,9 +3065,8 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) - + patch_whitelist(mocker, default_conf) default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0.05 default_conf['trailing_stop_positive_offset'] = 0.055 From ef1885c38bd9ab89df7418f6b8110e86e81f6398 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Oct 2019 13:24:26 +0200 Subject: [PATCH 041/319] Fix more tests --- tests/data/test_history.py | 11 ++--- tests/rpc/test_rpc_telegram.py | 73 ++++++++++++---------------------- tests/test_utils.py | 42 +++++++++---------- 3 files changed, 53 insertions(+), 73 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 95382768a..48ef2affd 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -533,21 +533,22 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + + ex = get_patched_exchange(mocker, default_conf) mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) ) - ex = get_patched_exchange(mocker, default_conf) timerange = TimeRange.parse_timerange("20190101-20190102") - unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"], + unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"], timeframes=["1m", "5m"], dl_path=testdatadir, timerange=timerange, erase=False ) assert dl_mock.call_count == 0 - assert "ETH/BTC" in unav_pairs - assert "XRP/BTC" in unav_pairs - assert log_has("Skipping pair ETH/BTC...", caplog) + assert "BTT/BTC" in unav_pairs + assert "LTC/USDT" in unav_pairs + assert log_has("Skipping pair BTT/BTC...", caplog) def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a776ad5df..766511d2d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -22,7 +22,7 @@ from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange, - patch_get_signal) + patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -143,17 +143,15 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: assert log_has('Exception occurred within Telegram module', caplog) -def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: +def test_status(default_conf, update, mocker, fee, ticker,) -> None: update.message.chat.id = 123 default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = 123 - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() status_table = MagicMock() @@ -184,9 +182,8 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: _status_table=status_table, _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -204,13 +201,11 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: assert status_table.call_count == 1 -def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_exchange(mocker) +def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() status_table = MagicMock() @@ -220,9 +215,9 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No _status_table=status_table, _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -256,14 +251,12 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] -def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_exchange(mocker) +def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() mocker.patch.multiple( @@ -271,10 +264,9 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) default_conf['stake_amount'] = 15.0 - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -307,8 +299,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) + limit_sell_order, mocker) -> None: default_conf['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -318,7 +309,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() mocker.patch.multiple( @@ -326,9 +316,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -382,7 +371,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker @@ -393,9 +381,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -420,14 +407,12 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() mocker.patch.multiple( @@ -435,9 +420,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -724,16 +708,16 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: def test_forcesell_handle(default_conf, update, ticker, fee, - ticker_sell_up, markets, mocker) -> None: + ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) + patch_whitelist(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), ) freqtradebot = FreqtradeBot(default_conf) @@ -775,17 +759,18 @@ def test_forcesell_handle(default_conf, update, ticker, fee, def test_forcesell_down_handle(default_conf, update, ticker, fee, - ticker_sell_down, markets, mocker) -> None: + ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) + patch_whitelist(mocker, default_conf) + mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), ) freqtradebot = FreqtradeBot(default_conf) @@ -830,17 +815,17 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, } == last_msg -def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: +def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + patch_whitelist(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), ) default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) @@ -885,9 +870,8 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - patch_exchange(mocker) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -980,8 +964,7 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non def test_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) + limit_buy_order, limit_sell_order, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -992,10 +975,8 @@ def test_performance_handle(default_conf, update, ticker, fee, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets), ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -1018,8 +999,7 @@ def test_performance_handle(default_conf, update, ticker, fee, assert 'ETH/BTC\t6.20% (1)' in msg_mock.call_args_list[0][0][0] -def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_exchange(mocker) +def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1030,10 +1010,9 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), - markets=PropertyMock(markets) + get_fee=fee, ) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) diff --git a/tests/test_utils.py b/tests/test_utils.py index f64a6924a..0833375c8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -188,8 +188,8 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 8 active markets: " - "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, TKN/BTC, XLTCUSDT.\n" + assert ("Exchange Bittrex has 9 active markets: " + "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n" in captured.out) patch_exchange(mocker, api_mock=api_mock, id="binance") @@ -202,7 +202,7 @@ def test_list_markets(mocker, markets, capsys): pargs['config'] = None start_list_markets(pargs, False) captured = capsys.readouterr() - assert re.match("\nExchange Binance has 8 active markets:\n", + assert re.match("\nExchange Binance has 9 active markets:\n", captured.out) patch_exchange(mocker, api_mock=api_mock, id="bittrex") @@ -227,8 +227,8 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), True) captured = capsys.readouterr() - assert ("Exchange Bittrex has 7 active pairs: " - "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, TKN/BTC.\n" + assert ("Exchange Bittrex has 8 active pairs: " + "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, NEO/BTC, TKN/BTC, XRP/BTC.\n" in captured.out) # Test list-pairs subcommand with --all: all pairs @@ -254,7 +254,7 @@ def test_list_markets(mocker, markets, capsys): start_list_markets(get_args(args), False) captured = capsys.readouterr() assert ("Exchange Bittrex has 5 active markets with ETH, LTC as base currencies: " - "ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, XLTCUSDT.\n" + "ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, XLTCUSDT.\n" in captured.out) # active markets, base=LTC @@ -267,7 +267,7 @@ def test_list_markets(mocker, markets, capsys): start_list_markets(get_args(args), False) captured = capsys.readouterr() assert ("Exchange Bittrex has 3 active markets with LTC as base currency: " - "LTC/USD, LTC/USDT, XLTCUSDT.\n" + "LTC/BTC, LTC/USD, XLTCUSDT.\n" in captured.out) # active markets, quote=USDT, USD @@ -279,8 +279,8 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 4 active markets with USDT, USD as quote currencies: " - "ETH/USDT, LTC/USD, LTC/USDT, XLTCUSDT.\n" + assert ("Exchange Bittrex has 3 active markets with USDT, USD as quote currencies: " + "ETH/USDT, LTC/USD, XLTCUSDT.\n" in captured.out) # active markets, quote=USDT @@ -292,8 +292,8 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 3 active markets with USDT as quote currency: " - "ETH/USDT, LTC/USDT, XLTCUSDT.\n" + assert ("Exchange Bittrex has 2 active markets with USDT as quote currency: " + "ETH/USDT, XLTCUSDT.\n" in captured.out) # active markets, base=LTC, quote=USDT @@ -305,21 +305,21 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 2 active markets with LTC as base currency and " - "with USDT as quote currency: LTC/USDT, XLTCUSDT.\n" + assert ("Exchange Bittrex has 1 active market with LTC as base currency and " + "with USDT as quote currency: XLTCUSDT.\n" in captured.out) # active pairs, base=LTC, quote=USDT args = [ '--config', 'config.json.example', "list-pairs", - "--base", "LTC", "--quote", "USDT", + "--base", "LTC", "--quote", "USD", "--print-list", ] start_list_markets(get_args(args), True) captured = capsys.readouterr() assert ("Exchange Bittrex has 1 active pair with LTC as base currency and " - "with USDT as quote currency: LTC/USDT.\n" + "with USD as quote currency: LTC/USD.\n" in captured.out) # active markets, base=LTC, quote=USDT, NONEXISTENT @@ -331,8 +331,8 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 2 active markets with LTC as base currency and " - "with USDT, NONEXISTENT as quote currencies: LTC/USDT, XLTCUSDT.\n" + assert ("Exchange Bittrex has 1 active market with LTC as base currency and " + "with USDT, NONEXISTENT as quote currencies: XLTCUSDT.\n" in captured.out) # active markets, base=LTC, quote=NONEXISTENT @@ -355,7 +355,7 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 8 active markets:\n" + assert ("Exchange Bittrex has 9 active markets:\n" in captured.out) # Test tabular output, no markets found @@ -378,7 +378,7 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ('["BLK/BTC","BTT/BTC","ETH/BTC","ETH/USDT","LTC/USD","LTC/USDT","TKN/BTC","XLTCUSDT"]' + assert ('["BLK/BTC","ETH/BTC","ETH/USDT","LTC/BTC","LTC/USD","NEO/BTC","TKN/BTC","XLTCUSDT","XRP/BTC"]' in captured.out) # Test --print-csv @@ -391,7 +391,7 @@ def test_list_markets(mocker, markets, capsys): captured = capsys.readouterr() assert ("Id,Symbol,Base,Quote,Active,Is pair" in captured.out) assert ("blkbtc,BLK/BTC,BLK,BTC,True,True" in captured.out) - assert ("BTTBTC,BTT/BTC,BTT,BTC,True,True" in captured.out) + assert ("USD-LTC,LTC/USD,LTC,USD,True,True" in captured.out) # Test --one-column args = [ @@ -402,7 +402,7 @@ def test_list_markets(mocker, markets, capsys): start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE) - assert re.search(r"^BTT/BTC$", captured.out, re.MULTILINE) + assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE) def test_create_datadir_failed(caplog): From 32df73c056763d1305f80dc37ba5edcaf55d0faa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Oct 2019 13:28:04 +0200 Subject: [PATCH 042/319] flake --- tests/conftest.py | 6 ++++-- tests/rpc/test_rpc.py | 3 +-- tests/test_freqtradebot.py | 2 +- tests/test_utils.py | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 84612175d..0d0511751 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,7 +63,8 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) if mock_markets: - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) + mocker.patch('freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=get_markets())) if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) @@ -71,7 +72,8 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', mock_markets=True) -> Exchange: +def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', + mock_markets=True) -> Exchange: patch_exchange(mocker, api_mock, id, mock_markets) config["exchange"]["name"] = id try: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a5da9b51e..df2261c1f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -9,12 +9,11 @@ from numpy import isnan from freqtrade import DependencyException, TemporaryError from freqtrade.edge import PairInfo -from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import patch_exchange, patch_get_signal, get_patched_freqtradebot +from tests.conftest import patch_get_signal, get_patched_freqtradebot # Functions for recurrent object patching diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ff9a34142..a58c12ead 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2951,7 +2951,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0.01 patch_whitelist(mocker, default_conf) - + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) diff --git a/tests/test_utils.py b/tests/test_utils.py index 0833375c8..7d6b82809 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -378,7 +378,8 @@ def test_list_markets(mocker, markets, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ('["BLK/BTC","ETH/BTC","ETH/USDT","LTC/BTC","LTC/USD","NEO/BTC","TKN/BTC","XLTCUSDT","XRP/BTC"]' + assert ('["BLK/BTC","ETH/BTC","ETH/USDT","LTC/BTC","LTC/USD","NEO/BTC",' + '"TKN/BTC","XLTCUSDT","XRP/BTC"]' in captured.out) # Test --print-csv From 13ae339a2e2c08344d7ec3224d0e0666fe453672 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Oct 2019 16:34:13 +0200 Subject: [PATCH 043/319] Improve windows Install documentation with hints --- docs/installation.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index fcbce571e..afc635673 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -218,6 +218,12 @@ If that is not available on your system, feel free to try the instructions below ### Install freqtrade manually +!!! Note + Make sure to use 64bit Windows to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. + +!!! Hint + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document. + #### Clone the git repository ```bash From 48d83715a5cff5f2ea68338fde92bff0b2e07105 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 27 Oct 2019 03:44:49 +0300 Subject: [PATCH 044/319] Fix typo in docs (thanks to Escaliert@Slack) --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index ff40b1750..5e936065c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -135,7 +135,7 @@ To allow the bot to trade all the available `stake_currency` in your account set In this case a trade amount is calclulated as: ```python -currency_balanse / (max_open_trades - current_open_trades) +currency_balance / (max_open_trades - current_open_trades) ``` ### Understand minimal_roi From e5487441ba29c93cc0658bd3e9a8f876482e6b4c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Oct 2019 06:08:55 +0100 Subject: [PATCH 045/319] Fix typos --- docs/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index afc635673..02870a1c1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -219,10 +219,10 @@ If that is not available on your system, feel free to try the instructions below ### Install freqtrade manually !!! Note - Make sure to use 64bit Windows to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. + Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. !!! Hint - Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document. + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document. #### Clone the git repository From 141c454187c96bd8d2f0f85b4309a18e2a8b1bfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Oct 2019 11:17:01 +0200 Subject: [PATCH 046/319] Add startup-candles-argument for strategy --- freqtrade/strategy/interface.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 014ca9968..48a70b0ce 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -103,6 +103,9 @@ class IStrategy(ABC): # run "populate_indicators" only for new candle process_only_new_candles: bool = False + # Count of candles the strategy requires before producing valid signals + startup_candle_count: int = 0 + # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. @@ -421,6 +424,7 @@ class IStrategy(ABC): def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """ Creates a dataframe and populates indicators for given ticker data + Used by optimize operations only, not during dry / live runs. """ return {pair: self.advise_indicators(pair_data, {'pair': pair}) for pair, pair_data in tickerdata.items()} From 616fe08bcea0dae438906aaff60434749ff4e375 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Oct 2019 11:29:25 +0200 Subject: [PATCH 047/319] Add subtract_start to timerange object --- freqtrade/configuration/timerange.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index fc759ab6e..527402af7 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -27,6 +27,10 @@ class TimeRange: return (self.starttype == other.starttype and self.stoptype == other.stoptype and self.startts == other.startts and self.stopts == other.stopts) + def subtract_start(self, seconds) -> None: + if self.startts: + self.startts = self.startts - seconds + @staticmethod def parse_timerange(text: Optional[str]): """ From 9e7e051eb42d45fffed0b8318587a0f8e7e78d99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Oct 2019 11:30:20 +0200 Subject: [PATCH 048/319] add trim-dataframe method --- freqtrade/data/history.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index ed5d80b0e..d385a28ed 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -13,6 +13,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple import arrow +import pytz from pandas import DataFrame from freqtrade import OperationalException, misc @@ -49,6 +50,19 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: return tickerlist[start_index:stop_index] +def trim_dataframe(df: DataFrame, timerange: TimeRange) -> DataFrame: + """ + Trim dataframe based on given timerange + """ + if timerange.starttype == 'date': + start = datetime.fromtimestamp(timerange.startts, tz=pytz.utc) + df = df.loc[df['date'] >= start, :] + if timerange.stoptype == 'date': + stop = datetime.fromtimestamp(timerange.stopts, tz=pytz.utc) + df = df.loc[df['date'] <= stop, :] + return df + + def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str, timerange: Optional[TimeRange] = None) -> Optional[list]: """ From 9c7696a8ce2e4833c4995b7e2ff30368b6292913 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Oct 2019 13:56:01 +0200 Subject: [PATCH 049/319] Add required_startup to backtesting --- freqtrade/optimize/backtesting.py | 33 ++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fb8c182ee..aa8a6a882 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -15,7 +15,7 @@ from freqtrade import OperationalException from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -90,6 +90,9 @@ class Backtesting: self.ticker_interval = str(self.config.get('ticker_interval')) self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) + # Get maximum required startup period + self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) + self.required_startup_s = self.required_startup * timeframe_to_seconds(self.ticker_interval) # Load one (first) strategy self._set_strategy(self.strategylist[0]) @@ -418,11 +421,19 @@ class Backtesting: timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) + + logger.info('Using indicator startup period: %s ...', self.required_startup) + + # Timerange_startup is timerange - startup-candles + timerange_startup = deepcopy(timerange) + timerange_startup.subtract_start(self.required_startup_s) + data = history.load_data( datadir=Path(self.config['datadir']), pairs=pairs, ticker_interval=self.ticker_interval, timerange=timerange, + startup_candles=self.required_startup ) if not data: @@ -439,11 +450,14 @@ class Backtesting: min_date, max_date = history.get_timeframe(data) logger.info( - 'Backtesting with data from %s up to %s (%s days)..', - min_date.isoformat(), - max_date.isoformat(), - (max_date - min_date).days + 'Loading backtest data from %s up to %s (%s days)..', + min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) + if not timerange_startup.starttype: + # If no startts was defined, we need to move the backtesting start + logger.info("Moving start-date by %s candles.", self.required_startup) + timerange.startts = min_date.timestamp + self.required_startup_s + timerange.starttype = 'date' for strat in self.strategylist: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) @@ -452,6 +466,15 @@ class Backtesting: # need to reprocess data every time to populate signals preprocessed = self.strategy.tickerdata_to_dataframe(data) + # Trim startup period from analyzed dataframe + for pair, df in preprocessed.items(): + preprocessed[pair] = history.trim_dataframe(df, timerange) + min_date, max_date = history.get_timeframe(preprocessed) + + logger.info( + 'Backtesting with data from %s up to %s (%s days)..', + min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days + ) # Execute backtest and print results all_results[self.strategy.get_strategy_name()] = self.backtest( { From 704121c197798c2c0ad3829ff464a997d60fa1ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Oct 2019 14:02:53 +0200 Subject: [PATCH 050/319] Move most logic to history --- freqtrade/data/history.py | 30 ++++++++++++++++++++++++------ freqtrade/optimize/backtesting.py | 12 ++++-------- tests/optimize/test_backtesting.py | 2 +- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index d385a28ed..71ac5c9a7 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -8,6 +8,7 @@ Includes: import logging import operator +from copy import deepcopy from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -19,7 +20,7 @@ from pandas import DataFrame from freqtrade import OperationalException, misc from freqtrade.configuration import TimeRange from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv -from freqtrade.exchange import Exchange, timeframe_to_minutes +from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_seconds logger = logging.getLogger(__name__) @@ -127,7 +128,8 @@ def load_pair_history(pair: str, refresh_pairs: bool = False, exchange: Optional[Exchange] = None, fill_up_missing: bool = True, - drop_incomplete: bool = True + drop_incomplete: bool = True, + startup_candles: int = 0, ) -> DataFrame: """ Loads cached ticker history for the given pair. @@ -140,9 +142,15 @@ def load_pair_history(pair: str, :param exchange: Exchange object (needed when using "refresh_pairs") :param fill_up_missing: Fill missing values with "No action"-candles :param drop_incomplete: Drop last candle assuming it may be incomplete. + :param startup_candles: Additional candles to load at the start of the period :return: DataFrame with ohlcv data """ + timerange_startup = deepcopy(timerange) + if startup_candles: + logger.info('Using indicator startup period: %s ...', startup_candles) + timerange_startup.subtract_start(timeframe_to_seconds(ticker_interval) * startup_candles) + # The user forced the refresh of pairs if refresh_pairs: download_pair_history(datadir=datadir, @@ -151,11 +159,11 @@ def load_pair_history(pair: str, ticker_interval=ticker_interval, timerange=timerange) - pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) + pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange_startup) if pairdata: - if timerange: - _validate_pairdata(pair, pairdata, timerange) + if timerange_startup: + _validate_pairdata(pair, pairdata, timerange_startup) return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair, fill_missing=fill_up_missing, drop_incomplete=drop_incomplete) @@ -174,10 +182,20 @@ def load_data(datadir: Path, exchange: Optional[Exchange] = None, timerange: Optional[TimeRange] = None, fill_up_missing: bool = True, + startup_candles: int = 0, ) -> Dict[str, DataFrame]: """ Loads ticker history data for a list of pairs - :return: dict(:) + :param datadir: Path to the data storage location. + :param ticker_interval: Ticker-interval (e.g. "5m") + :param pairs: List of pairs to load + :param refresh_pairs: Refresh pairs from exchange. + (Note: Requires exchange to be passed as well.) + :param exchange: Exchange object (needed when using "refresh_pairs") + :param timerange: Limit data to be loaded to this timerange + :param fill_up_missing: Fill missing values with "No action"-candles + :param startup_candles: Additional candles to load at the start of the period + :return: dict(:) TODO: refresh_pairs is still used by edge to keep the data uptodate. This should be replaced in the future. Instead, writing the current candles to disk from dataprovider should be implemented, as this would avoid loading ohlcv data twice. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aa8a6a882..59130dbc0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -92,7 +92,6 @@ class Backtesting: # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) - self.required_startup_s = self.required_startup * timeframe_to_seconds(self.ticker_interval) # Load one (first) strategy self._set_strategy(self.strategylist[0]) @@ -422,11 +421,6 @@ class Backtesting: timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) - logger.info('Using indicator startup period: %s ...', self.required_startup) - - # Timerange_startup is timerange - startup-candles - timerange_startup = deepcopy(timerange) - timerange_startup.subtract_start(self.required_startup_s) data = history.load_data( datadir=Path(self.config['datadir']), @@ -453,10 +447,12 @@ class Backtesting: 'Loading backtest data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) - if not timerange_startup.starttype: + if not timerange.starttype: # If no startts was defined, we need to move the backtesting start logger.info("Moving start-date by %s candles.", self.required_startup) - timerange.startts = min_date.timestamp + self.required_startup_s + timerange.startts = (min_date.timestamp + + timeframe_to_seconds(self.ticker_interval) + * self.required_startup) timerange.starttype = 'date' for strat in self.strategylist: diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 998edda8a..3353274ef 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -117,7 +117,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, - timerange=None, exchange=None, live=False): + timerange=None, exchange=None, live=False, startup_candles=0): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC", fill_missing=True)} From 6382a4cd042ed1e84af48493c60e7229986a0e11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Oct 2019 17:57:38 +0200 Subject: [PATCH 051/319] Implement startup-period to default-strategy --- freqtrade/optimize/backtesting.py | 9 +++++---- freqtrade/strategy/default_strategy.py | 6 +++--- tests/optimize/test_backtesting.py | 10 ++++++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 59130dbc0..6b82dd601 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -421,7 +421,6 @@ class Backtesting: 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']), pairs=pairs, @@ -447,9 +446,11 @@ class Backtesting: 'Loading backtest data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) - if not timerange.starttype: - # If no startts was defined, we need to move the backtesting start - logger.info("Moving start-date by %s candles.", self.required_startup) + if (not timerange.starttype or (self.required_startup + and min_date.timestamp == timerange.startts)): + # If no startts was defined, or test-data starts at the defined test-date + logger.warning("Moving start-date by %s candles to account for startup time.", + self.required_startup) timerange.startts = (min_date.timestamp + timeframe_to_seconds(self.ticker_interval) * self.required_startup) diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index b839a9618..0a241691c 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -39,6 +39,9 @@ class DefaultStrategy(IStrategy): 'stoploss_on_exchange': False } + # Count of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + # Optional time in force for orders order_time_in_force = { 'buy': 'gtc', @@ -105,9 +108,6 @@ class DefaultStrategy(IStrategy): # EMA - Exponential Moving Average dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - # SMA - Simple Moving Average - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - return dataframe def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 3353274ef..b14209e2d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -838,7 +838,10 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' + 'Backtesting with data from 2017-11-14T21:37:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Moving start-date by 20 candles to account for startup time.', + 'Loading backtest 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 ...' ] @@ -892,7 +895,10 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' + 'Backtesting with data from 2017-11-14T21:37:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Moving start-date by 20 candles to account for startup time.', + 'Loading backtest 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', From 5c2682e2c94828f1cbfa32b9ec6534fae103caf1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Oct 2019 17:58:26 +0200 Subject: [PATCH 052/319] Add startup_candle_count to sample strategy --- freqtrade/strategy/default_strategy.py | 2 +- user_data/strategies/sample_strategy.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index 0a241691c..6c343b477 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -39,7 +39,7 @@ class DefaultStrategy(IStrategy): 'stoploss_on_exchange': False } - # Count of candles the strategy requires before producing valid signals + # Number of candles the strategy requires before producing valid signals startup_candle_count: int = 20 # Optional time in force for orders diff --git a/user_data/strategies/sample_strategy.py b/user_data/strategies/sample_strategy.py index 80c30283d..c2fd681d2 100644 --- a/user_data/strategies/sample_strategy.py +++ b/user_data/strategies/sample_strategy.py @@ -59,6 +59,9 @@ class SampleStrategy(IStrategy): sell_profit_only = False ignore_roi_if_buy_signal = False + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + # Optional order type mapping. order_types = { 'buy': 'limit', From bd4a23beeb9506b75b4556176ae3eef26924e0ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Oct 2019 19:30:01 +0200 Subject: [PATCH 053/319] Refactor start-adjust logic to timerange --- freqtrade/configuration/timerange.py | 27 +++++++++++++++++++++++++++ freqtrade/optimize/backtesting.py | 12 +++--------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 527402af7..5731631c5 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -1,11 +1,14 @@ """ This module contains the argument manager class """ +import logging import re from typing import Optional import arrow +logger = logging.getLogger(__name__) + class TimeRange: """ @@ -28,9 +31,33 @@ class TimeRange: and self.startts == other.startts and self.stopts == other.stopts) def subtract_start(self, seconds) -> None: + """ + Subtracts from startts if startts is set. + :param seconds: Seconds to subtract from starttime + :return: None (Modifies the object in place) + """ if self.startts: self.startts = self.startts - seconds + def adjust_start_if_necessary(self, ticker_interval_secs: int, startup_candles: int, + min_date: arrow.Arrow) -> None: + """ + Adjust startts by candles. + Applies only if no startup-candles have been available. + :param ticker_interval_secs: Ticker interval in seconds e.g. `timeframe_to_seconds('5m')` + :param startup_candles: Number of candles to move start-date forward + :param min_date: Minimum data date loaded. Key kriterium to decide if start-time + has to be moved + :return: None (Modifies the object in place) + """ + if (not self.starttype or (startup_candles + and min_date.timestamp == self.startts)): + # If no startts was defined, or test-data starts at the defined test-date + logger.warning("Moving start-date by %s candles to account for startup time.", + startup_candles) + self.startts = (min_date.timestamp + ticker_interval_secs * startup_candles) + self.starttype = 'date' + @staticmethod def parse_timerange(text: Optional[str]): """ diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6b82dd601..1d6b328a8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -446,15 +446,9 @@ class Backtesting: 'Loading backtest data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) - if (not timerange.starttype or (self.required_startup - and min_date.timestamp == timerange.startts)): - # If no startts was defined, or test-data starts at the defined test-date - logger.warning("Moving start-date by %s candles to account for startup time.", - self.required_startup) - timerange.startts = (min_date.timestamp - + timeframe_to_seconds(self.ticker_interval) - * self.required_startup) - timerange.starttype = 'date' + # Adjust startts forward if not enough data is available + timerange.adjust_start_if_necessary(timeframe_to_seconds(self.ticker_interval), + self.required_startup, min_date) for strat in self.strategylist: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) From 5cdae17d19b75b07aed188fd676b056a0ec4e7a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Oct 2019 19:41:10 +0200 Subject: [PATCH 054/319] Add tests for timerange modifications --- tests/test_timerange.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/test_timerange.py b/tests/test_timerange.py index 4851cbebd..d758092ed 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -1,10 +1,11 @@ # pragma pylint: disable=missing-docstring, C0103 +import arrow import pytest from freqtrade.configuration import TimeRange -def test_parse_timerange_incorrect() -> None: +def test_parse_timerange_incorrect(): assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-') assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522') @@ -28,3 +29,37 @@ def test_parse_timerange_incorrect() -> None: with pytest.raises(Exception, match=r'Incorrect syntax.*'): TimeRange.parse_timerange('-') + + +def test_subtract_start(): + x = TimeRange('date', 'date', 1274486400, 1438214400) + x.subtract_start(300) + assert x.startts == 1274486400 - 300 + + # Do nothing if no startdate exists + x = TimeRange(None, 'date', 0, 1438214400) + x.subtract_start(300) + assert not x.startts + + x = TimeRange('date', None, 1274486400, 0) + x.subtract_start(300) + assert x.startts == 1274486400 - 300 + + +def test_adjust_start_if_necessary(): + min_date = arrow.Arrow(2017, 11, 14, 21, 15, 00) + + x = TimeRange('date', 'date', 1510694100, 1510780500) + # Adjust by 20 candles - min_date == startts + x.adjust_start_if_necessary(300, 20, min_date) + assert x.startts == 1510694100 + (20 * 300) + + x = TimeRange('date', 'date', 1510700100, 1510780500) + # Do nothing, startupe is set and different min_date + x.adjust_start_if_necessary(300, 20, min_date) + assert x.startts == 1510694100 + (20 * 300) + + x = TimeRange(None, 'date', 0, 1510780500) + # Adjust by 20 candles = 20 * 5m + x.adjust_start_if_necessary(300, 20, min_date) + assert x.startts == 1510694100 + (20 * 300) From 86624411c6e5af7c6fa1c1dc9f184f5420ef1172 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Oct 2019 19:54:43 +0200 Subject: [PATCH 055/319] Test trim_dataframe --- tests/data/test_history.py | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 95382768a..d7e0562cc 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -427,6 +427,46 @@ def test_trim_tickerlist(testdatadir) -> None: assert not ticker +def test_trim_dataframe(testdatadir) -> None: + data = history.load_data( + datadir=testdatadir, + ticker_interval='1m', + pairs=['UNITTEST/BTC'] + )['UNITTEST/BTC'] + min_date = int(data.iloc[0]['date'].timestamp()) + max_date = int(data.iloc[-1]['date'].timestamp()) + data_modify = data.copy() + + # Remove first 30 minutes (1800 s) + tr = TimeRange('date', None, min_date + 1800, 0) + data_modify = history.trim_dataframe(data_modify, tr) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 30 + assert all(data_modify.iloc[-1] == data.iloc[-1]) + assert all(data_modify.iloc[0] == data.iloc[30]) + + data_modify = data.copy() + # Remove last 30 minutes (1800 s) + tr = TimeRange(None, 'date', 0, max_date - 1800) + data_modify = history.trim_dataframe(data_modify, tr) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 30 + assert all(data_modify.iloc[0] == data.iloc[0]) + assert all(data_modify.iloc[-1] == data.iloc[-31]) + + data_modify = data.copy() + # Remove first 25 and last 30 minutes (1800 s) + tr = TimeRange('date', 'date', min_date + 1500, max_date - 1800) + data_modify = history.trim_dataframe(data_modify, tr) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 55 + # first row matches 25th original row + assert all(data_modify.iloc[0] == data.iloc[25]) + + def test_file_dump_json_tofile(testdatadir) -> None: file = testdatadir / 'test_{id}.json'.format(id=str(uuid.uuid4())) data = {'bar': 'foo'} From 33164ac78ee2d72647931467abcb8d60e2d5e54e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Oct 2019 20:13:43 +0200 Subject: [PATCH 056/319] Refactor loading of bt data to backtesting ... --- freqtrade/data/history.py | 8 ++++- freqtrade/optimize/backtesting.py | 55 ++++++++++++++++--------------- freqtrade/optimize/hyperopt.py | 25 ++++---------- 3 files changed, 42 insertions(+), 46 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 71ac5c9a7..dfd175b1f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -183,6 +183,7 @@ def load_data(datadir: Path, timerange: Optional[TimeRange] = None, fill_up_missing: bool = True, startup_candles: int = 0, + fail_without_data: bool = False ) -> Dict[str, DataFrame]: """ Loads ticker history data for a list of pairs @@ -195,6 +196,7 @@ def load_data(datadir: Path, :param timerange: Limit data to be loaded to this timerange :param fill_up_missing: Fill missing values with "No action"-candles :param startup_candles: Additional candles to load at the start of the period + :param fail_without_data: Raise OperationalException if no data is found. :return: dict(:) TODO: refresh_pairs is still used by edge to keep the data uptodate. This should be replaced in the future. Instead, writing the current candles to disk @@ -208,9 +210,13 @@ def load_data(datadir: Path, datadir=datadir, timerange=timerange, refresh_pairs=refresh_pairs, exchange=exchange, - fill_up_missing=fill_up_missing) + fill_up_missing=fill_up_missing, + startup_candles=startup_candles) if hist is not None: result[pair] = hist + + if fail_without_data and not result: + raise OperationalException("No data found. Terminating.") return result diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1d6b328a8..fe31912bc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -105,6 +105,31 @@ class Backtesting: # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + def load_bt_data(self): + 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']), + pairs=self.config['exchange']['pair_whitelist'], + ticker_interval=self.ticker_interval, + timerange=timerange, + startup_candles=self.required_startup, + fail_without_data=True, + ) + + min_date, max_date = history.get_timeframe(data) + + logger.info( + 'Loading data from %s up to %s (%s days)..', + min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days + ) + # Adjust startts forward if not enough data is available + timerange.adjust_start_if_necessary(timeframe_to_seconds(self.ticker_interval), + self.required_startup, min_date) + + return data, timerange + def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame, skip_nan: bool = False) -> str: """ @@ -414,42 +439,18 @@ class Backtesting: :return: None """ data: Dict[str, Any] = {} - pairs = self.config['exchange']['pair_whitelist'] logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - - 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']), - pairs=pairs, - ticker_interval=self.ticker_interval, - timerange=timerange, - startup_candles=self.required_startup - ) - - if not data: - logger.critical("No data found. Terminating.") - return # Use max_open_trades in backtesting, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): max_open_trades = self.config['max_open_trades'] else: logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') max_open_trades = 0 + + data, timerange = self.load_bt_data() + all_results = {} - - min_date, max_date = history.get_timeframe(data) - - logger.info( - 'Loading backtest data from %s up to %s (%s days)..', - min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days - ) - # Adjust startts forward if not enough data is available - timerange.adjust_start_if_necessary(timeframe_to_seconds(self.ticker_interval), - self.required_startup, min_date) - for strat in self.strategylist: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 07258a048..2264234d4 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from skopt import Optimizer from skopt.space import Dimension from freqtrade.configuration import TimeRange -from freqtrade.data.history import load_data, get_timeframe +from freqtrade.data.history import load_data, get_timeframe, trim_dataframe from freqtrade.misc import round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules @@ -379,30 +379,19 @@ class Hyperopt: ) def start(self) -> None: - 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']), - pairs=self.config['exchange']['pair_whitelist'], - ticker_interval=self.backtesting.ticker_interval, - timerange=timerange - ) + data, timerange = self.backtesting.load_bt_data() - if not data: - logger.critical("No data found. Terminating.") - return + preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) + # Trim startup period from analyzed dataframe + for pair, df in preprocessed.items(): + preprocessed[pair] = trim_dataframe(df, timerange) min_date, max_date = get_timeframe(data) logger.info( 'Hyperopting with data from %s up to %s (%s days)..', - min_date.isoformat(), - max_date.isoformat(), - (max_date - min_date).days + min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) - - preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) - dump(preprocessed, self.tickerdata_pickle) # We don't need exchange instance anymore while running hyperopt From 2ba388074e577278a83a17eb92e3980176f75b56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Oct 2019 20:27:51 +0200 Subject: [PATCH 057/319] Fix small bugs --- freqtrade/configuration/timerange.py | 2 +- tests/optimize/test_backtesting.py | 20 ++++++++------------ tests/optimize/test_hyperopt.py | 7 +++---- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 5731631c5..df5c937cf 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -51,7 +51,7 @@ class TimeRange: :return: None (Modifies the object in place) """ if (not self.starttype or (startup_candles - and min_date.timestamp == self.startts)): + and min_date.timestamp >= self.startts)): # If no startts was defined, or test-data starts at the defined test-date logger.warning("Moving start-date by %s candles to account for startup time.", startup_candles) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index b14209e2d..ba87848ec 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -117,7 +117,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, - timerange=None, exchange=None, live=False, startup_candles=0): + timerange=None, exchange=None, live=False, *args, **kwargs): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC", fill_missing=True)} @@ -494,7 +494,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> def get_timeframe(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) - mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) + mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=None)) mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) @@ -511,10 +511,8 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> default_conf['timerange'] = '20180101-20180102' backtesting = Backtesting(default_conf) - backtesting.start() - # check the logs, that will contain the backtest result - - assert log_has('No data found. Terminating.', caplog) + with pytest.raises(OperationalException, match='No data found. Terminating.'): + backtesting.start() def test_backtest(default_conf, fee, mocker, testdatadir) -> None: @@ -838,10 +836,9 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Backtesting with data from 2017-11-14T21:37:00+00:00 ' + 'Loading data from 2017-11-14T20:57:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', - 'Moving start-date by 20 candles to account for startup time.', - 'Loading backtest data from 2017-11-14T21:17: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 ...' ] @@ -895,10 +892,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Backtesting with data from 2017-11-14T21:37:00+00:00 ' + 'Loading data from 2017-11-14T20:57:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', - 'Moving start-date by 20 candles to account for startup time.', - 'Loading backtest data from 2017-11-14T21:17: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/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 052c3ba77..d1448d367 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -228,7 +228,7 @@ def test_start(mocker, default_conf, caplog) -> None: def test_start_no_data(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={})) + mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=None)) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -242,9 +242,8 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - start_hyperopt(args) - - assert log_has('No data found. Terminating.', caplog) + with pytest.raises(OperationalException, match='No data found. Terminating.'): + start_hyperopt(args) def test_start_filelock(mocker, default_conf, caplog) -> None: From 2bc74882e9940dd6e5c67e94240e7031ed2a9ec1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Oct 2019 10:00:44 +0100 Subject: [PATCH 058/319] Add test for startup_candles --- tests/data/test_history.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index d7e0562cc..057524fb3 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -95,6 +95,23 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> N _clean_test_file(file) +def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> None: + ltfmock = mocker.patch('freqtrade.data.history.load_tickerdata_file', + MagicMock(return_value=None)) + timerange = TimeRange('date', None, 1510639620, 0) + history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='1m', + datadir=testdatadir, timerange=timerange, + startup_candles=20, + ) + assert log_has( + 'Using indicator startup period: 20 ...', caplog + ) + assert ltfmock.call_count == 1 + assert ltfmock.call_args_list[0][1]['timerange'] != timerange + # startts is 20 minutes earlier + assert ltfmock.call_args_list[0][1]['timerange'].startts == timerange.startts - 20 * 60 + + def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, default_conf, testdatadir) -> None: """ From c4cb098d14cafe4760551974b6de6c309888166e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Oct 2019 10:17:02 +0100 Subject: [PATCH 059/319] Update documentation with indicator_startup_period --- docs/backtesting.md | 2 ++ docs/strategy-customization.md | 35 ++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 34c5f1fbe..6d21fa2bf 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -72,6 +72,8 @@ The exported trades can be used for [further analysis](#further-backtest-result- freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json ``` +Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period). + #### Supplying custom fee value Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index ab7dcfc30..ae44d32ea 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -117,6 +117,37 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). Then uncomment indicators you need. +### Strategy startup period + +Most indicators have an "instable period", in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be. +To account for this, the strategy has an attribute, `startup_candle_count`. +This should be set to the maximum number of candles that the strategy requires to calculate stable indicators. + +In this example strategy, this should be set to 100 (`startup_candle_count = 100`), since the longest needed history is 100 candles. + +``` python + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) +``` + +By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt. + +!!! Warning: + `startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during trading operations. + +#### Example + +Let's try to backtest 1 month (January 2019) of 5m candles. + +``` bash +freqtrade backtesting --timerange 20190101-20190201 --ticker-interval 5m +``` + +Since backtesting knows it needs 100 candles to generate valid buy-signals, it'll load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00. +If this data is available, Indicators will be calculated with this extended timerange. The startup period (Up to 2019-01-01 00:00:00) will then be removed before starting backtesting. + +!!! Note + If data for the startup-period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00. + ### Buy signal rules Edit the method `populate_buy_trend()` in your strategy file to update your buy strategy. @@ -267,10 +298,10 @@ class Awesomestrategy(IStrategy): ``` !!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. !!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. ### Additional data (DataProvider) From 223f0cd4d3780a348f1a743329cf671b38ec914d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Oct 2019 10:18:58 +0100 Subject: [PATCH 060/319] Apply startup_period to edge as well --- freqtrade/edge/__init__.py | 3 ++- tests/edge/test_edge.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 2655fbc65..883bf4a0f 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -100,7 +100,8 @@ class Edge: ticker_interval=self.strategy.ticker_interval, refresh_pairs=self._refresh_pairs, exchange=self.exchange, - timerange=self._timerange + timerange=self._timerange, + startup_candles=self.strategy.startup_candle_count, ) if not data: diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 5e244a97e..e1af50768 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -256,7 +256,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, - timerange=None, exchange=None): + timerange=None, exchange=None, *args, **kwargs): hz = 0.1 base = 0.001 From 73f5bff9c5c9487457f529a007cef99faff6f1f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Oct 2019 10:38:21 +0100 Subject: [PATCH 061/319] Add validation to make sure strategies work on that exchange --- freqtrade/exchange/exchange.py | 10 ++++++++++ freqtrade/resolvers/strategy_resolver.py | 1 + tests/exchange/test_exchange.py | 14 ++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 71f0737ef..3fd7d615c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -228,6 +228,7 @@ class Exchange: self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) + self.validate_required_startup_candles(config.get('startup_candle_count', 0)) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -443,6 +444,15 @@ class Exchange: raise OperationalException( f'Time in force policies are not supported for {self.name} yet.') + def validate_required_startup_candles(self, startup_candles) -> None: + """ + Checks if required startup_candles is more than ohlcv_candle_limit. + """ + if startup_candles + 5 > self._ft_has['ohlcv_candle_limit']: + raise OperationalException( + f"This strategy requires {startup_candles} candles to start. " + f"{self.name} only provides {self._ft_has['ohlcv_candle_limit']}.") + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index d6fbe9a7a..5bea74027 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -57,6 +57,7 @@ class StrategyResolver(IResolver): ("order_time_in_force", None, False), ("stake_currency", None, False), ("stake_amount", None, False), + ("startup_candle_count", None, False), ("use_sell_signal", True, True), ("sell_profit_only", False, True), ("ignore_roi_if_buy_signal", False, True), diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1e0a5fdc3..e0e0cc7b1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -533,6 +533,20 @@ def test_validate_order_types_not_in_config(default_conf, mocker): Exchange(conf) +def test_validate_required_startup_candles(default_conf, mocker, caplog): + api_mock = MagicMock() + default_conf['startup_candle_count'] = 2000 + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + + with pytest.raises(OperationalException, match=r'This strategy requires 2000.*'): + Exchange(default_conf) + + def test_exchange_has(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf) assert not exchange.exchange_has('ASDFASDF') From 132a4da7cf6ce448877506d28e7fbc9b2b63701d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Oct 2019 10:56:38 +0100 Subject: [PATCH 062/319] Small style fixes and adjusted tests --- docs/strategy-customization.md | 2 +- freqtrade/data/history.py | 2 +- freqtrade/exchange/exchange.py | 1 + freqtrade/optimize/hyperopt.py | 3 +-- freqtrade/strategy/interface.py | 2 +- tests/exchange/test_exchange.py | 8 ++++++-- tests/optimize/test_hyperopt.py | 24 ++++++++++++++++-------- 7 files changed, 27 insertions(+), 15 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index ae44d32ea..4d3d9bce5 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -131,7 +131,7 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100 By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt. -!!! Warning: +!!! Warning `startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during trading operations. #### Example diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index dfd175b1f..c07b58da2 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -147,7 +147,7 @@ def load_pair_history(pair: str, """ timerange_startup = deepcopy(timerange) - if startup_candles: + if startup_candles and timerange_startup: logger.info('Using indicator startup period: %s ...', startup_candles) timerange_startup.subtract_start(timeframe_to_seconds(ticker_interval) * startup_candles) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3fd7d615c..023e16cc5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -447,6 +447,7 @@ class Exchange: def validate_required_startup_candles(self, startup_candles) -> None: """ Checks if required startup_candles is more than ohlcv_candle_limit. + Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. """ if startup_candles + 5 > self._ft_has['ohlcv_candle_limit']: raise OperationalException( diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 2264234d4..c576ea6f8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -22,8 +22,7 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade.configuration import TimeRange -from freqtrade.data.history import load_data, get_timeframe, trim_dataframe +from freqtrade.data.history import get_timeframe, trim_dataframe from freqtrade.misc import round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 48a70b0ce..d42f8e989 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -424,7 +424,7 @@ class IStrategy(ABC): def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """ Creates a dataframe and populates indicators for given ticker data - Used by optimize operations only, not during dry / live runs. + Used by optimize operations only, not during dry / live runs. """ return {pair: self.advise_indicators(pair_data, {'pair': pair}) for pair, pair_data in tickerdata.items()} diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e0e0cc7b1..6805d8e73 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -535,7 +535,6 @@ def test_validate_order_types_not_in_config(default_conf, mocker): def test_validate_required_startup_candles(default_conf, mocker, caplog): api_mock = MagicMock() - default_conf['startup_candle_count'] = 2000 mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) @@ -543,7 +542,12 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - with pytest.raises(OperationalException, match=r'This strategy requires 2000.*'): + default_conf['startup_candle_count'] = 20 + ex = Exchange(default_conf) + assert ex + default_conf['startup_candle_count'] = 600 + + with pytest.raises(OperationalException, match=r'This strategy requires 600.*'): Exchange(default_conf) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d1448d367..d0c37c40d 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -392,7 +392,8 @@ def test_roi_table_generation(hyperopt) -> 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('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -607,7 +608,8 @@ 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.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -644,7 +646,8 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: 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.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -681,7 +684,8 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> def test_simplified_interface_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.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -727,7 +731,8 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -756,7 +761,8 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - def test_simplified_interface_buy(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.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -801,7 +807,8 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: def test_simplified_interface_sell(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.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -852,7 +859,8 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None ]) def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, method, space) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) From 2af3ce3ecc5b3af96401ab36014a137a75c6691b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Oct 2019 19:36:00 +0100 Subject: [PATCH 063/319] Improve stoploss documentation - split out offset_is_reached --- docs/stoploss.md | 74 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index f5e2f8df6..3ea8beb72 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -3,74 +3,98 @@ The `stoploss` configuration parameter is loss in percentage that should trigger a sale. For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. -Most of the strategy files already include the optimal `stoploss` -value. This parameter is optional. If you use it in the configuration file, it will take over the -`stoploss` value from the strategy file. +Most of the strategy files already include the optimal `stoploss` value. +Stoploss parameters need to be set in either strategy or configuration file. +Parameters in the configuration will overwrite settings within the strategy. -## Stop Loss support +## Stop Loss Types At this stage the bot contains the following stoploss support modes: 1. static stop loss, defined in either the strategy or configuration. 2. trailing stop loss, defined in the configuration. 3. trailing stop loss, custom positive loss, defined in configuration. +4. trailing stop loss only once the trade has reached a certain offset, !!! Note All stoploss properties can be configured in either Strategy or configuration. Configuration values override strategy values. -Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfuly. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. +Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. -In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. As an example in case of trailing stoploss if the order is on the exchange and the market is going up then the bot automatically cancels the previous stoploss order and put a new one with a stop value higher than previous one. It is clear that the bot cannot do it every 5 seconds otherwise it gets banned. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). +In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. + +For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. +The bot cannot do this every 5 seconds, otherwise it would get banned by the exchange. +So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). +This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. !!! Note Stoploss on exchange is only supported for Binance as of now. ## Static Stop Loss -This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which -will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss. +This is very simple, you define a stop loss of x. This will try to sell the asset once the loss exceeds the defined loss. ## Trailing Stop Loss -The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally. +The initial value for this is `stoploss`, set either in the strategy or in the configuration file. Just as you would define your Stop loss normally. To enable this Feauture all you have to do is to define the configuration element: ``` json "trailing_stop" : True ``` -This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases. +This will now activate an algorithm, which automatically moves the stop loss up every time the price of your asset increases. -For example, simplified math, +For example, simplified math: -* you buy an asset at a price of 100$ -* your stop loss is defined at 2% -* which means your stop loss, gets triggered once your asset dropped below 98$ -* assuming your asset now increases to 102$ -* your stop loss, will now be 2% of 102$ or 99.96$ -* now your asset drops in value to 101$, your stop loss, will still be 99.96$ +* the bot buys an asset at a price of 100$ +* the stop loss is defined at 2% +* the stop loss would get triggered once the asset dropps below 98$ +* assuming the asset now increases to 102$ +* the stop loss will now be 2% of 102$ or 99.96$ +* now the asset drops in value to 101$, the stop loss, will still be 99.96$, and would trigger at 99.96$. -basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price +In summary: The stoploss will be adjusted to be always be 2% of the highest observed price. -### Custom positive loss +### Custom positive stoploss -Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, -the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you have 1.1% profit, -it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them. +It is also possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, the system will utilize a new stop loss, which can have a different value. +For example your default stop loss is 5%, but once you have 1.1% profit, it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them. -Both values can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true. +Both values can be configured in the strategy or configuration file and requires `"trailing_stop": true` to be set to true. ``` json "trailing_stop_positive": 0.01, "trailing_stop_positive_offset": 0.011, - "trailing_only_offset_is_reached": false ``` The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit. You should also make sure to have this value (`trailing_stop_positive_offset`) lower than your minimal ROI, otherwise minimal ROI will apply first and sell your trade. -If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured`stoploss`. +### Trailing only once offset is reached + +It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market goes down again. + +If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. +This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. + +``` json + "trailing_only_offset_is_reached": true, +``` + +Simplified example: + +```python + stoploss = 0.05 + trailing_stop_positive_offset = 0.03 + trailing_only_offset_is_reached = True +``` + +* the bot buys an asset at a price of 100$ +* the stop loss is defined at 5% +* the stop loss will remain at 95% until profit reaches +3% ## Changing stoploss on open trades From 70ad909b16e15bd7c436a6704f825f2f7c64e3a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Oct 2019 19:46:05 +0100 Subject: [PATCH 064/319] change samples to python code, and simplify a few things --- docs/stoploss.md | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 3ea8beb72..7cf885590 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -4,20 +4,18 @@ The `stoploss` configuration parameter is loss in percentage that should trigger For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. Most of the strategy files already include the optimal `stoploss` value. -Stoploss parameters need to be set in either strategy or configuration file. -Parameters in the configuration will overwrite settings within the strategy. + +!!! Info + All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. Configuration values will override the strategy values. ## Stop Loss Types At this stage the bot contains the following stoploss support modes: -1. static stop loss, defined in either the strategy or configuration. -2. trailing stop loss, defined in the configuration. -3. trailing stop loss, custom positive loss, defined in configuration. -4. trailing stop loss only once the trade has reached a certain offset, - -!!! Note - All stoploss properties can be configured in either Strategy or configuration. Configuration values override strategy values. +1. Static stop loss. +2. Trailing stop loss. +3. Trailing stop loss, custom positive loss. +4. Trailing stop loss only once the trade has reached a certain offset, Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. @@ -37,11 +35,11 @@ This is very simple, you define a stop loss of x. This will try to sell the asse ## Trailing Stop Loss -The initial value for this is `stoploss`, set either in the strategy or in the configuration file. Just as you would define your Stop loss normally. -To enable this Feauture all you have to do is to define the configuration element: +The initial value for this is `stoploss`, just as you would define your static Stop loss. +To enable trailing stoploss: -``` json -"trailing_stop" : True +``` python +trailing_stop = True ``` This will now activate an algorithm, which automatically moves the stop loss up every time the price of your asset increases. @@ -62,31 +60,36 @@ In summary: The stoploss will be adjusted to be always be 2% of the highest obse It is also possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, the system will utilize a new stop loss, which can have a different value. For example your default stop loss is 5%, but once you have 1.1% profit, it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them. -Both values can be configured in the strategy or configuration file and requires `"trailing_stop": true` to be set to true. +Both values require `trailing_stop` to be set to true. -``` json - "trailing_stop_positive": 0.01, - "trailing_stop_positive_offset": 0.011, +``` python + trailing_stop_positive = 0.01 + trailing_stop_positive_offset = 0.011 ``` The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit. +Before this, `stoploss` is used for the trailing stoploss. -You should also make sure to have this value (`trailing_stop_positive_offset`) lower than your minimal ROI, otherwise minimal ROI will apply first and sell your trade. +Read the [next section](#trailing-only-once-offset-is-reached) to keep stoploss at 5% of the entry point. + +!!! Tip + Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. ### Trailing only once offset is reached -It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market goes down again. +It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns. If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. -``` json - "trailing_only_offset_is_reached": true, +``` python + trailing_stop_positive_offset = 0.011 + trailing_only_offset_is_reached = true ``` Simplified example: -```python +``` python stoploss = 0.05 trailing_stop_positive_offset = 0.03 trailing_only_offset_is_reached = True From 46b975a49158f5c7caaf827312c85e712222059d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2019 07:40:15 +0000 Subject: [PATCH 065/319] Bump pytest-mock from 1.11.1 to 1.11.2 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 1.11.1 to 1.11.2. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v1.11.1...v1.11.2) 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 f5cde59e8..ada602360 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.740 pytest==5.2.1 pytest-asyncio==0.10.0 pytest-cov==2.8.1 -pytest-mock==1.11.1 +pytest-mock==1.11.2 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From 60b99469b938c2dae88faf2f2b30129d51a163d5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2019 07:40:57 +0000 Subject: [PATCH 066/319] Bump nbconvert from 5.6.0 to 5.6.1 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 5.6.0 to 5.6.1. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/5.6.0...5.6.1) 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 f5cde59e8..d9c7467a9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,4 +15,4 @@ pytest-mock==1.11.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents -nbconvert==5.6.0 +nbconvert==5.6.1 From 44d0a6f2b89b69ecc2a4bddfdf35ce9093b0521b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2019 07:42:58 +0000 Subject: [PATCH 067/319] Bump ccxt from 1.18.1306 to 1.18.1346 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1306 to 1.18.1346. - [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.1306...1.18.1346) 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 1e42d8a04..64a43ee62 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.1306 +ccxt==1.18.1346 SQLAlchemy==1.3.10 python-telegram-bot==12.2.0 arrow==0.15.2 From 596a269dfd23b1e687201b1390580da6977d3793 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2019 11:41:44 +0000 Subject: [PATCH 068/319] Bump pytest from 5.2.1 to 5.2.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.2.1 to 5.2.2. - [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.2.1...5.2.2) 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 7e4736372..589ca7c54 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.0.0 mypy==0.740 -pytest==5.2.1 +pytest==5.2.2 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==1.11.2 From 3a6020dcd76ce4b661b8ca7e79e7a64c49391c14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 12:43:35 +0100 Subject: [PATCH 069/319] small improvements to stoploss doc --- docs/stoploss.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 7cf885590..105488296 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -15,14 +15,14 @@ At this stage the bot contains the following stoploss support modes: 1. Static stop loss. 2. Trailing stop loss. 3. Trailing stop loss, custom positive loss. -4. Trailing stop loss only once the trade has reached a certain offset, +4. Trailing stop loss only once the trade has reached a certain offset. Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. -The bot cannot do this every 5 seconds, otherwise it would get banned by the exchange. +The bot cannot do this every 5 seconds (at each iteration), otherwise it would get banned by the exchange. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. @@ -31,7 +31,7 @@ This same logic will reapply a stoploss order on the exchange should you cancel ## Static Stop Loss -This is very simple, you define a stop loss of x. This will try to sell the asset once the loss exceeds the defined loss. +This is very simple, you define a stop loss of x (as a ratio of price, i.e. x * 100% of price). This will try to sell the asset once the loss exceeds the defined loss. ## Trailing Stop Loss @@ -51,7 +51,7 @@ For example, simplified math: * the stop loss would get triggered once the asset dropps below 98$ * assuming the asset now increases to 102$ * the stop loss will now be 2% of 102$ or 99.96$ -* now the asset drops in value to 101$, the stop loss, will still be 99.96$, and would trigger at 99.96$. +* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$. In summary: The stoploss will be adjusted to be always be 2% of the highest observed price. From 73343b338732da04056f372a47a40e8dec254ed5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 12:53:12 +0100 Subject: [PATCH 070/319] Address feedback --- docs/configuration.md | 2 +- tests/conftest.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 90f2687d0..bfe900d3e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -75,7 +75,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** | `exchange.password` | '' | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used when using VolumePairList (see [below](#dynamic-pairlists)). +| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). | `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) diff --git a/tests/conftest.py b/tests/conftest.py index 0d0511751..4feae6a60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -129,8 +129,7 @@ def patch_freqtradebot(mocker, config) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot._refresh_whitelist', - MagicMock(return_value=config['exchange']['pair_whitelist'])) + patch_whitelist(mocker, config) def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: From 61c037f2cf0bb6a43026bc43ad3e2ed4312d0d2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 13:05:54 +0100 Subject: [PATCH 071/319] Fix some typos and comment mistakes --- docs/strategy-customization.md | 14 +++++++------- freqtrade/configuration/timerange.py | 2 +- freqtrade/data/history.py | 2 +- tests/test_timerange.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4d3d9bce5..cef362ffd 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -119,8 +119,8 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame ### Strategy startup period -Most indicators have an "instable period", in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be. -To account for this, the strategy has an attribute, `startup_candle_count`. +Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be. +To account for this, the strategy can be assigned the `startup_candle_count` attribute. This should be set to the maximum number of candles that the strategy requires to calculate stable indicators. In this example strategy, this should be set to 100 (`startup_candle_count = 100`), since the longest needed history is 100 candles. @@ -132,21 +132,21 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100 By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt. !!! Warning - `startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during trading operations. + `startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations. #### Example -Let's try to backtest 1 month (January 2019) of 5m candles. +Let's try to backtest 1 month (January 2019) of 5m candles using the an example strategy with EMA100, as above. ``` bash freqtrade backtesting --timerange 20190101-20190201 --ticker-interval 5m ``` -Since backtesting knows it needs 100 candles to generate valid buy-signals, it'll load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00. -If this data is available, Indicators will be calculated with this extended timerange. The startup period (Up to 2019-01-01 00:00:00) will then be removed before starting backtesting. +Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00. +If this data is available, indicators will be calculated with this extended timerange. The instable startup period (up to 2019-01-01 00:00:00) will then be removed before starting backtesting. !!! Note - If data for the startup-period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00. + If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00. ### Buy signal rules diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index df5c937cf..156f0e1e2 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -52,7 +52,7 @@ class TimeRange: """ if (not self.starttype or (startup_candles and min_date.timestamp >= self.startts)): - # If no startts was defined, or test-data starts at the defined test-date + # If no startts was defined, or backtest-data starts at the defined backtest-date logger.warning("Moving start-date by %s candles to account for startup time.", startup_candles) self.startts = (min_date.timestamp + ticker_interval_secs * startup_candles) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index c07b58da2..412b086c0 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -147,7 +147,7 @@ def load_pair_history(pair: str, """ timerange_startup = deepcopy(timerange) - if startup_candles and timerange_startup: + if startup_candles > 0 and timerange_startup: logger.info('Using indicator startup period: %s ...', startup_candles) timerange_startup.subtract_start(timeframe_to_seconds(ticker_interval) * startup_candles) diff --git a/tests/test_timerange.py b/tests/test_timerange.py index d758092ed..5c35535f0 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -55,7 +55,7 @@ def test_adjust_start_if_necessary(): assert x.startts == 1510694100 + (20 * 300) x = TimeRange('date', 'date', 1510700100, 1510780500) - # Do nothing, startupe is set and different min_date + # Do nothing, startup is set and different min_date x.adjust_start_if_necessary(300, 20, min_date) assert x.startts == 1510694100 + (20 * 300) From e82460bde67475968c2b934b29ab8d9844fc8e82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 14:24:12 +0100 Subject: [PATCH 072/319] Fix create_cum_profit to work with trades that don't open on candle opens --- freqtrade/data/btanalysis.py | 12 +++++++++--- freqtrade/plot/plotting.py | 10 +++++----- tests/data/test_btanalysis.py | 2 +- tests/test_plotting.py | 4 ++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 17abae3b6..0f5d395ff 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -150,15 +150,21 @@ def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "c return df_comb -def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame: +def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, + timeframe: str) -> pd.DataFrame: """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index :param trades: DataFrame containing trades (requires columns close_time and profitperc) + :param col_name: Column name that will be assigned the results + :param timeframe: Timeframe used during the operations :return: Returns df with one additional column, col_name, containing the cumulative profit. """ - # Use groupby/sum().cumsum() to avoid errors when multiple trades sold at the same candle. - df[col_name] = trades.groupby('close_time')['profitperc'].sum().cumsum() + from freqtrade.exchange import timeframe_to_minutes + ticker_minutes = timeframe_to_minutes(timeframe) + # Resample to ticker_interval to make sure trades match candles + _trades_sum = trades.resample(f'{ticker_minutes}min', on='close_time')[['profitperc']].sum() + df.loc[:, col_name] = _trades_sum.cumsum() # Set first value to 0 df.loc[df.iloc[0].name, col_name] = 0 # FFill to get continuous diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 6bd5993b6..bbdb52ca1 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -264,12 +264,12 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], - trades: pd.DataFrame) -> go.Figure: + trades: pd.DataFrame, timeframe: str) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" df_comb = combine_tickers_with_mean(tickers, "close") # Add combined cumulative profit - df_comb = create_cum_profit(df_comb, trades, 'cum_profit') + df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe) # Plot the pairs average close prices, and total profit growth avgclose = go.Scatter( @@ -293,7 +293,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], for pair in pairs: profit_col = f'cum_profit_{pair}' - df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) + df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col, timeframe) fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") @@ -382,9 +382,9 @@ def plot_profit(config: Dict[str, Any]) -> None: ) # Filter trades to relevant pairs trades = trades[trades['pair'].isin(plot_elements["pairs"])] - # 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) + fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], + trades, config.get('ticker_interval', '5m')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / "plot", auto_open=True) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 4068e00e4..a7d646823 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -130,7 +130,7 @@ def test_create_cum_profit(testdatadir): cum_profits = create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'POWR/BTC'], - "cum_profits") + "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index a39b2b76e..1c7d1b392 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -234,7 +234,7 @@ def test_add_profit(testdatadir): cum_profits = create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'POWR/BTC'], - "cum_profits") + "cum_profits", timeframe="5m") fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits') figure = fig1.layout.figure @@ -256,7 +256,7 @@ def test_generate_profit_graph(testdatadir): ) trades = trades[trades['pair'].isin(pairs)] - fig = generate_profit_graph(pairs, tickers, trades) + fig = generate_profit_graph(pairs, tickers, trades, timeframe="5m") assert isinstance(fig, go.Figure) assert fig.layout.title.text == "Freqtrade Profit plot" From 069da224bc28ce199a76908748120c84db2b54a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 14:30:01 +0100 Subject: [PATCH 073/319] Add test to verify this is correct --- tests/data/test_btanalysis.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index a7d646823..a04a2c529 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import pytest from arrow import Arrow -from pandas import DataFrame, to_datetime +from pandas import DataFrame, DateOffset, to_datetime from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, @@ -134,3 +134,21 @@ def test_create_cum_profit(testdatadir): assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 + + +def test_create_cum_profit1(testdatadir): + filename = testdatadir / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + # Move close-time to "off" the candle, to make sure the logic still works + bt_data.loc[:, 'close_time'] = bt_data.loc[:, 'close_time'] + DateOffset(seconds=20) + timerange = TimeRange.parse_timerange("20180110-20180112") + + df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + datadir=testdatadir, timerange=timerange) + + cum_profits = create_cum_profit(df.set_index('date'), + bt_data[bt_data["pair"] == 'POWR/BTC'], + "cum_profits", timeframe="5m") + assert "cum_profits" in cum_profits.columns + assert cum_profits.iloc[0]['cum_profits'] == 0 + assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 From 466a3b87fcda0001ea4325ba0aef2c0457d59679 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 16:13:31 +0100 Subject: [PATCH 074/319] Enhance tests to cover precision_filter correctly --- tests/conftest.py | 60 +++++++++++++++++++++++++++++++++ tests/pairlist/test_pairlist.py | 33 +++++++++++------- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4feae6a60..a291a6676 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -572,6 +572,44 @@ def get_markets(): } +@pytest.fixture +def shitcoinmarkets(markets): + """ + Fixture with shitcoin markets - used to test filters in pairlists + """ + shitmarkets = deepcopy(markets) + shitmarkets.update({'HOT/BTC': { + 'id': 'HOTBTC', + 'symbol': 'HOT/BTC', + 'base': 'HOT', + 'quote': 'BTC', + 'active': True, + 'precision': { + 'base': 8, + 'quote': 8, + 'amount': 0, + 'price': 8 + }, + 'limits': { + 'amount': { + 'min': 1.0, + 'max': 90000000.0 + }, + 'price': { + 'min': None, + 'max': None + }, + 'cost': { + 'min': 0.001, + 'max': None + } + }, + 'info': {}, + }, + }) + return shitmarkets + + @pytest.fixture def markets_empty(): return MagicMock(return_value=[]) @@ -866,6 +904,28 @@ def tickers(): 'quoteVolume': 1215.14489611, 'info': {} }, + 'HOT/BTC': { + 'symbol': 'HOT/BTC', + 'timestamp': 1572273518661, + 'datetime': '2019-10-28T14:38:38.661Z', + 'high': 0.00000011, + 'low': 0.00000009, + 'bid': 0.0000001, + 'bidVolume': 1476027288.0, + 'ask': 0.00000011, + 'askVolume': 820153831.0, + 'vwap': 0.0000001, + 'open': 0.00000009, + 'close': 0.00000011, + 'last': 0.00000011, + 'previousClose': 0.00000009, + 'change': 0.00000002, + 'percentage': 22.222, + 'average': None, + 'baseVolume': 1442290324.0, + 'quoteVolume': 143.78311994, + 'info': {} + }, 'ETH/USDT': { 'symbol': 'ETH/USDT', 'timestamp': 1522014804118, diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 929fc0ba0..6f050a77d 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -2,11 +2,12 @@ from unittest.mock import MagicMock, PropertyMock +import pytest + from freqtrade import OperationalException from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.resolvers import PairListResolver -from tests.conftest import get_patched_freqtradebot -import pytest +from tests.conftest import get_patched_freqtradebot, log_has_re # whitelist, blacklist @@ -67,20 +68,25 @@ def test_refresh_pairlists(mocker, markets, whitelist_conf): assert whitelist_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist -def test_refresh_pairlist_dynamic(mocker, markets, tickers, whitelist_conf): +def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): whitelist_conf['pairlist'] = {'method': 'VolumePairList', - 'config': {'number_assets': 5} + 'config': {'number_assets': 5, + 'precision_filter': False} } + mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_tickers=tickers, exchange_has=MagicMock(return_value=True) ) freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) - + # Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=shitcoinmarkets), + ) # argument: use the whitelist dynamically by exchange-volume - whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'] + whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC'] freqtradebot.pairlists.refresh_pairlist() assert whitelist == freqtradebot.pairlists.whitelist @@ -108,19 +114,20 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): @pytest.mark.parametrize("precision_filter,base_currency,key,whitelist_result", [ - (False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), - (False, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC']), + (False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC']), + (False, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'HOT/BTC']), (False, "USDT", "quoteVolume", ['ETH/USDT']), (False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange (True, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC"]), (True, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC"]) ]) -def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers, base_currency, key, - whitelist_result, precision_filter) -> None: +def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, + precision_filter, base_currency, key, whitelist_result, + caplog) -> None: whitelist_conf['pairlist']['method'] = 'VolumePairList' mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=shitcoinmarkets)) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, p, r: round(r, 8)) @@ -128,6 +135,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers, freqtrade.config['stake_currency'] = base_currency whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key) assert sorted(whitelist) == sorted(whitelist_result) + if precision_filter: + assert log_has_re(r'^Removed .* from whitelist, because stop price .* would be <= stop limit.*', caplog) def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: From 4ff035537b01cf454a3cb34b60f232ad48972a2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 16:21:00 +0100 Subject: [PATCH 075/319] Simplify precision_filter code --- config_full.json.example | 2 +- docs/configuration.md | 5 ++--- freqtrade/pairlist/VolumePairList.py | 21 +++++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 5789e49ac..a5af0f7a6 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -55,7 +55,7 @@ "config": { "number_assets": 20, "sort_key": "quoteVolume", - "precision_filter": false + "precision_filter": true } }, "exchange": { diff --git a/docs/configuration.md b/docs/configuration.md index 1ad13c87a..03f15e07d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -423,8 +423,7 @@ section of the configuration. * `VolumePairList` * It selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. - * There is a possibility to filter low-value coins that would not allow setting a stop loss -(set `precision_filter` parameter to `true` for this). + * By default, low-value coins that would not allow setting a stop loss are filtered out. (set `precision_filter` parameter to `false` to disable this behaviour). * `VolumePairList` does not consider `pair_whitelist`, but builds this automatically based the pairlist configuration. * Pairs in `pair_blacklist` are not considered for VolumePairList, even if all other filters would match. @@ -440,7 +439,7 @@ Example: "config": { "number_assets": 20, "sort_key": "quoteVolume", - "precision_filter": false + "precision_filter": true } }, ``` diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 5f53cd17b..5e20f0fb1 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -26,7 +26,7 @@ class VolumePairList(IPairList): 'for "pairlist.config.number_assets"') self._number_pairs = self._whitelistconf['number_assets'] self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume') - self._precision_filter = self._whitelistconf.get('precision_filter', False) + self._precision_filter = self._whitelistconf.get('precision_filter', True) if not self._freqtrade.exchange.exchange_has('fetchTickers'): raise OperationalException( @@ -76,18 +76,19 @@ class VolumePairList(IPairList): valid_tickers = [t for t in sorted_tickers if t["symbol"] in valid_pairs] if self._freqtrade.strategy.stoploss is not None and self._precision_filter: + # Precalculate correct stoploss value + stoploss = 1 - abs(self._freqtrade.strategy.stoploss) - stop_prices = [self._freqtrade.get_target_bid(t["symbol"], t) - * (1 - abs(self._freqtrade.strategy.stoploss)) for t in valid_tickers] - rates = [sp * 0.99 for sp in stop_prices] - logger.debug("\n".join([f"{sp} : {r}" for sp, r in zip(stop_prices[:10], rates[:10])])) for i, t in enumerate(valid_tickers): - sp = self._freqtrade.exchange.symbol_price_prec(t["symbol"], stop_prices[i]) - r = self._freqtrade.exchange.symbol_price_prec(t["symbol"], rates[i]) - logger.debug(f"{t['symbol']} - {sp} : {r}") - if sp <= r: + stop_price = (self._freqtrade.get_target_bid(t["symbol"], t) * stoploss) + # Adjust stop-prices to precision + sp = self._freqtrade.exchange.symbol_price_prec(t["symbol"], stop_price) + stop_gap_price = self._freqtrade.exchange.symbol_price_prec(t["symbol"], + stop_price * 0.99) + logger.debug(f"{t['symbol']} - {sp} : {stop_gap_price}") + if sp <= stop_gap_price: logger.info(f"Removed {t['symbol']} from whitelist, " - f"because stop price {sp} would be <= stop limit {r}") + f"because stop price {sp} would be <= stop limit {stop_gap_price}") valid_tickers.remove(t) pairs = [s['symbol'] for s in valid_tickers] From d706571e6f9d29b6f843377bb7a2c0a62e778e6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 16:25:01 +0100 Subject: [PATCH 076/319] Extract precision_filter to seperate function --- freqtrade/pairlist/VolumePairList.py | 42 +++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 5e20f0fb1..44dbd0ecf 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -56,6 +56,27 @@ class VolumePairList(IPairList): self._whitelist = self._gen_pair_whitelist( self._config['stake_currency'], self._sort_key) + def _validate_precision_filter(self, ticker: dict, stoploss: float) -> bool: + """ + Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very + low value pairs. + :param ticker: ticker dict as returned from ccxt.load_markets() + :param stoploss: stoploss value as set in the configuration + (already cleaned to be guaranteed negative) + :return: True if the pair can stay, false if it should be removed + """ + stop_price = (self._freqtrade.get_target_bid(ticker["symbol"], ticker) * stoploss) + # Adjust stop-prices to precision + sp = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], stop_price) + stop_gap_price = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], + stop_price * 0.99) + logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") + if sp <= stop_gap_price: + logger.info(f"Removed {ticker['symbol']} from whitelist, " + f"because stop price {sp} would be <= stop limit {stop_gap_price}") + return False + return True + @cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: """ @@ -75,21 +96,16 @@ class VolumePairList(IPairList): valid_pairs = self._validate_whitelist([s['symbol'] for s in sorted_tickers]) valid_tickers = [t for t in sorted_tickers if t["symbol"] in valid_pairs] - if self._freqtrade.strategy.stoploss is not None and self._precision_filter: - # Precalculate correct stoploss value + stoploss = None + if self._freqtrade.strategy.stoploss is not None: + # Precalculate sanitized stoploss value to avoid recalculation for every pair stoploss = 1 - abs(self._freqtrade.strategy.stoploss) - for i, t in enumerate(valid_tickers): - stop_price = (self._freqtrade.get_target_bid(t["symbol"], t) * stoploss) - # Adjust stop-prices to precision - sp = self._freqtrade.exchange.symbol_price_prec(t["symbol"], stop_price) - stop_gap_price = self._freqtrade.exchange.symbol_price_prec(t["symbol"], - stop_price * 0.99) - logger.debug(f"{t['symbol']} - {sp} : {stop_gap_price}") - if sp <= stop_gap_price: - logger.info(f"Removed {t['symbol']} from whitelist, " - f"because stop price {sp} would be <= stop limit {stop_gap_price}") - valid_tickers.remove(t) + for t in valid_tickers: + # Filter out assets which would not allow setting a stoploss + if (stoploss and self._precision_filter + and not self._validate_precision_filter(t, stoploss)): + valid_tickers.remove(t) pairs = [s['symbol'] for s in valid_tickers] logger.info(f"Searching pairs: {pairs[:self._number_pairs]}") From d803d86f4d846ba8e67c2bde84b0de5efaea1ee6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 06:53:26 +0100 Subject: [PATCH 077/319] Add low_price_percent_filter --- config_full.json.example | 3 ++- docs/configuration.md | 5 ++++- freqtrade/pairlist/VolumePairList.py | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index a5af0f7a6..5ae8021d5 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -55,7 +55,8 @@ "config": { "number_assets": 20, "sort_key": "quoteVolume", - "precision_filter": true + "precision_filter": true, + "low_price_percent_filter": null } }, "exchange": { diff --git a/docs/configuration.md b/docs/configuration.md index 03f15e07d..c7e0dac31 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -426,6 +426,8 @@ section of the configuration. * By default, low-value coins that would not allow setting a stop loss are filtered out. (set `precision_filter` parameter to `false` to disable this behaviour). * `VolumePairList` does not consider `pair_whitelist`, but builds this automatically based the pairlist configuration. * Pairs in `pair_blacklist` are not considered for VolumePairList, even if all other filters would match. + * `low_price_percent_filter` allows filtering of pairs where a raise of 1 price unit is below the `low_price_percent_filter` ratio. + Calculation example: Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. Example: @@ -439,7 +441,8 @@ Example: "config": { "number_assets": 20, "sort_key": "quoteVolume", - "precision_filter": true + "precision_filter": true, + "low_price_percent_filter": 0.03 } }, ``` diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 44dbd0ecf..4a6768efa 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -27,6 +27,8 @@ class VolumePairList(IPairList): self._number_pairs = self._whitelistconf['number_assets'] self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume') self._precision_filter = self._whitelistconf.get('precision_filter', True) + self._low_price_percent_filter = self._whitelistconf.get('low_price_percent_filter', None) + print(self._whitelistconf) if not self._freqtrade.exchange.exchange_has('fetchTickers'): raise OperationalException( @@ -77,6 +79,23 @@ class VolumePairList(IPairList): return False return True + def _validate_precision_filter_lowprice(self, ticker) -> bool: + """ + Check if if one price-step is > than a certain barrier. + :param ticker: ticker dict as returned from ccxt.load_markets() + :param precision: Precision + :return: True if the pair can stay, false if it should be removed + """ + precision = self._freqtrade.exchange.markets[ticker['symbol']]['precision']['price'] + + compare = ticker['last'] + 1 / pow(10, precision) + changeperc = (compare - ticker['last']) / ticker['last'] + if changeperc > self._low_price_percent_filter: + logger.info(f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%") + return False + return True + @cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: """ @@ -106,6 +125,10 @@ class VolumePairList(IPairList): if (stoploss and self._precision_filter and not self._validate_precision_filter(t, stoploss)): valid_tickers.remove(t) + continue + if self._low_price_percent_filter and not self._validate_precision_filter_lowprice(t,): + valid_tickers.remove(t) + continue pairs = [s['symbol'] for s in valid_tickers] logger.info(f"Searching pairs: {pairs[:self._number_pairs]}") From de2cc58b0cf0024256bd0c2c924d080838b5b86f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 10:39:27 +0100 Subject: [PATCH 078/319] Final cleanups and added tests --- config_full.json.example | 2 +- docs/configuration.md | 4 ++- freqtrade/pairlist/VolumePairList.py | 15 +++++---- tests/conftest.py | 50 ++++++++++++++++++++++++++++ tests/pairlist/test_pairlist.py | 36 ++++++++++++-------- 5 files changed, 85 insertions(+), 22 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 5ae8021d5..5935de392 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -56,7 +56,7 @@ "number_assets": 20, "sort_key": "quoteVolume", "precision_filter": true, - "low_price_percent_filter": null + "low_price_percent_filter": 0 } }, "exchange": { diff --git a/docs/configuration.md b/docs/configuration.md index c7e0dac31..7ccb6840a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -427,7 +427,9 @@ section of the configuration. * `VolumePairList` does not consider `pair_whitelist`, but builds this automatically based the pairlist configuration. * Pairs in `pair_blacklist` are not considered for VolumePairList, even if all other filters would match. * `low_price_percent_filter` allows filtering of pairs where a raise of 1 price unit is below the `low_price_percent_filter` ratio. + This option is disabled by default, and will only apply if set to <> 0. Calculation example: Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. + Example: @@ -442,7 +444,7 @@ Example: "number_assets": 20, "sort_key": "quoteVolume", "precision_filter": true, - "low_price_percent_filter": 0.03 + "low_price_percent_filter": 0.05 } }, ``` diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 4a6768efa..7c17c6d57 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -5,11 +5,14 @@ Provides lists as configured in config.json """ import logging +from copy import deepcopy from typing import List + from cachetools import TTLCache, cached -from freqtrade.pairlist.IPairList import IPairList from freqtrade import OperationalException +from freqtrade.pairlist.IPairList import IPairList + logger = logging.getLogger(__name__) SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] @@ -28,7 +31,6 @@ class VolumePairList(IPairList): self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume') self._precision_filter = self._whitelistconf.get('precision_filter', True) self._low_price_percent_filter = self._whitelistconf.get('low_price_percent_filter', None) - print(self._whitelistconf) if not self._freqtrade.exchange.exchange_has('fetchTickers'): raise OperationalException( @@ -64,10 +66,10 @@ class VolumePairList(IPairList): low value pairs. :param ticker: ticker dict as returned from ccxt.load_markets() :param stoploss: stoploss value as set in the configuration - (already cleaned to be guaranteed negative) + (already cleaned to be 1 - stoploss) :return: True if the pair can stay, false if it should be removed """ - stop_price = (self._freqtrade.get_target_bid(ticker["symbol"], ticker) * stoploss) + stop_price = self._freqtrade.get_target_bid(ticker["symbol"], ticker) * stoploss # Adjust stop-prices to precision sp = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], stop_price) stop_gap_price = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], @@ -120,10 +122,11 @@ class VolumePairList(IPairList): # Precalculate sanitized stoploss value to avoid recalculation for every pair stoploss = 1 - abs(self._freqtrade.strategy.stoploss) - for t in valid_tickers: + # Copy list since we're modifying this list + for t in deepcopy(valid_tickers): # Filter out assets which would not allow setting a stoploss if (stoploss and self._precision_filter - and not self._validate_precision_filter(t, stoploss)): + and not self._validate_precision_filter(t, stoploss)): valid_tickers.remove(t) continue if self._low_price_percent_filter and not self._validate_precision_filter_lowprice(t,): diff --git a/tests/conftest.py b/tests/conftest.py index a291a6676..d551596f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -606,6 +606,34 @@ def shitcoinmarkets(markets): }, 'info': {}, }, + 'FUEL/BTC': { + 'id': 'FUELBTC', + 'symbol': 'FUEL/BTC', + 'base': 'FUEL', + 'quote': 'BTC', + 'active': True, + 'precision': { + 'base': 8, + 'quote': 8, + 'amount': 0, + 'price': 8 + }, + 'limits': { + 'amount': { + 'min': 1.0, + 'max': 90000000.0 + }, + 'price': { + 'min': 1e-08, + 'max': 1000.0 + }, + 'cost': { + 'min': 0.001, + 'max': None + } + }, + 'info': {}, + }, }) return shitmarkets @@ -926,6 +954,28 @@ def tickers(): 'quoteVolume': 143.78311994, 'info': {} }, + 'FUEL/BTC': { + 'symbol': 'FUEL/BTC', + 'timestamp': 1572340250771, + 'datetime': '2019-10-29T09:10:50.771Z', + 'high': 0.00000040, + 'low': 0.00000035, + 'bid': 0.00000036, + 'bidVolume': 8932318.0, + 'ask': 0.00000037, + 'askVolume': 10140774.0, + 'vwap': 0.00000037, + 'open': 0.00000039, + 'close': 0.00000037, + 'last': 0.00000037, + 'previousClose': 0.00000038, + 'change': -0.00000002, + 'percentage': -5.128, + 'average': None, + 'baseVolume': 168927742.0, + 'quoteVolume': 62.68220262, + 'info': {} + }, 'ETH/USDT': { 'symbol': 'ETH/USDT', 'timestamp': 1522014804118, diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 6f050a77d..ac27d1c8e 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -26,7 +26,7 @@ def whitelist_conf(default_conf): 'BLK/BTC' ] default_conf['pairlist'] = {'method': 'StaticPairList', - 'config': {'number_assets': 3} + 'config': {'number_assets': 5} } return default_conf @@ -86,7 +86,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co markets=PropertyMock(return_value=shitcoinmarkets), ) # argument: use the whitelist dynamically by exchange-volume - whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC'] + whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC'] freqtradebot.pairlists.refresh_pairlist() assert whitelist == freqtradebot.pairlists.whitelist @@ -113,30 +113,38 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): assert set(whitelist) == set(pairslist) -@pytest.mark.parametrize("precision_filter,base_currency,key,whitelist_result", [ - (False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC']), - (False, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'HOT/BTC']), - (False, "USDT", "quoteVolume", ['ETH/USDT']), - (False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange - (True, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC"]), - (True, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC"]) +@pytest.mark.parametrize("precision_filter,low_price_filter,base_currency,key,whitelist_result", [ + (False, 0, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), + (False, 0, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'HOT/BTC', 'FUEL/BTC']), + (False, 0, "USDT", "quoteVolume", ['ETH/USDT']), + (False, 0, "ETH", "quoteVolume", []), + (True, 0, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC", 'FUEL/BTC']), + (True, 0, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC", 'FUEL/BTC']), + (False, 0.03, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'FUEL/BTC']), + # Hot is removed by precision_filter, Fuel by low_price_filter. + (True, 0.02, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, - precision_filter, base_currency, key, whitelist_result, - caplog) -> None: + precision_filter, low_price_filter, base_currency, key, + whitelist_result, caplog) -> None: whitelist_conf['pairlist']['method'] = 'VolumePairList' + whitelist_conf['pairlist']['config']['precision_filter'] = precision_filter + whitelist_conf['pairlist']['config']['low_price_percent_filter'] = low_price_filter + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=shitcoinmarkets)) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, p, r: round(r, 8)) - freqtrade.pairlists._precision_filter = precision_filter freqtrade.config['stake_currency'] = base_currency whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key) assert sorted(whitelist) == sorted(whitelist_result) if precision_filter: - assert log_has_re(r'^Removed .* from whitelist, because stop price .* would be <= stop limit.*', caplog) + assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' + r'would be <= stop limit.*', caplog) + + if low_price_filter: + assert log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: From 9c180e587bb96bc9d70dc257ca4390ac513aa5cc Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 30 Oct 2019 04:04:28 +0300 Subject: [PATCH 079/319] Log to stderr --- freqtrade/loggers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 90b8905e5..e00f4fc11 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -33,8 +33,8 @@ def setup_logging(config: Dict[str, Any]) -> None: # Log level verbosity = config['verbosity'] - # Log to stdout, not stderr - log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] + # Log to stderr + log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stderr)] if config.get('logfile'): log_handlers.append(RotatingFileHandler(config['logfile'], From 6fe7b13e37bb4b198c4f01a3a59bdd33534991de Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 09:20:56 +0100 Subject: [PATCH 080/319] Replace coins in whitelist with existing ones --- config.json.example | 2 +- config_full.json.example | 2 +- tests/config_test_comments.json | 2 +- tests/data/test_btanalysis.py | 8 ++++---- tests/optimize/test_hyperopt.py | 2 +- tests/test_plotting.py | 10 +++++----- tests/testdata/{POWR_BTC-5m.json => TRX_BTC-5m.json} | 0 tests/testdata/backtest-result_test.json | 2 +- tests/testdata/pairs.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) rename tests/testdata/{POWR_BTC-5m.json => TRX_BTC-5m.json} (100%) diff --git a/config.json.example b/config.json.example index 419019030..9a6dafd04 100644 --- a/config.json.example +++ b/config.json.example @@ -44,7 +44,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], diff --git a/config_full.json.example b/config_full.json.example index 5789e49ac..5edc19419 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -78,7 +78,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 8af39d6ba..8f41b08fa 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -78,7 +78,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index a04a2c529..78781cffd 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -125,11 +125,11 @@ def test_create_cum_profit(testdatadir): bt_data = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + df = load_pair_history(pair="TRX/BTC", ticker_interval='5m', datadir=testdatadir, timerange=timerange) cum_profits = create_cum_profit(df.set_index('date'), - bt_data[bt_data["pair"] == 'POWR/BTC'], + bt_data[bt_data["pair"] == 'TRX/BTC'], "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 @@ -143,11 +143,11 @@ def test_create_cum_profit1(testdatadir): bt_data.loc[:, 'close_time'] = bt_data.loc[:, 'close_time'] + DateOffset(seconds=20) timerange = TimeRange.parse_timerange("20180110-20180112") - df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + df = load_pair_history(pair="TRX/BTC", ticker_interval='5m', datadir=testdatadir, timerange=timerange) cum_profits = create_cum_profit(df.set_index('date'), - bt_data[bt_data["pair"] == 'POWR/BTC'], + bt_data[bt_data["pair"] == 'TRX/BTC'], "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d0c37c40d..675bbd62e 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -516,7 +516,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: default_conf.update({'hyperopt_min_trades': 1}) trades = [ - ('POWR/BTC', 0.023117, 0.000233, 100) + ('TRX/BTC', 0.023117, 0.000233, 100) ] labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] backtest_result = pd.DataFrame.from_records(trades, columns=labels) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 1c7d1b392..4a6efcd8e 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -53,10 +53,10 @@ def test_init_plotscript(default_conf, mocker, testdatadir): assert "trades" in ret assert "pairs" in ret - default_conf['pairs'] = ["POWR/BTC", "ADA/BTC"] + default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] ret = init_plotscript(default_conf) assert "tickers" in ret - assert "POWR/BTC" in ret["tickers"] + assert "TRX/BTC" in ret["tickers"] assert "ADA/BTC" in ret["tickers"] @@ -228,12 +228,12 @@ def test_add_profit(testdatadir): bt_data = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m', + df = history.load_pair_history(pair="TRX/BTC", ticker_interval='5m', datadir=testdatadir, timerange=timerange) fig = generate_empty_figure() cum_profits = create_cum_profit(df.set_index('date'), - bt_data[bt_data["pair"] == 'POWR/BTC'], + bt_data[bt_data["pair"] == 'TRX/BTC'], "cum_profits", timeframe="5m") fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits') @@ -247,7 +247,7 @@ def test_generate_profit_graph(testdatadir): filename = testdatadir / "backtest-result_test.json" trades = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - pairs = ["POWR/BTC", "ADA/BTC"] + pairs = ["TRX/BTC", "ADA/BTC"] tickers = history.load_data(datadir=testdatadir, pairs=pairs, diff --git a/tests/testdata/POWR_BTC-5m.json b/tests/testdata/TRX_BTC-5m.json similarity index 100% rename from tests/testdata/POWR_BTC-5m.json rename to tests/testdata/TRX_BTC-5m.json diff --git a/tests/testdata/backtest-result_test.json b/tests/testdata/backtest-result_test.json index 8701451dc..dce22acaf 100644 --- a/tests/testdata/backtest-result_test.json +++ b/tests/testdata/backtest-result_test.json @@ -1 +1 @@ -[["POWR/BTC",0.03990025,1515568500.0,1515568800.0,27,5,9.64e-05,0.00010074887218045112,false,"roi"],["ADA/BTC",0.03990025,1515568500.0,1515569400.0,27,15,4.756e-05,4.9705563909774425e-05,false,"roi"],["XLM/BTC",0.03990025,1515569100.0,1515569700.0,29,10,3.339e-05,3.489631578947368e-05,false,"roi"],["POWR/BTC",0.03990025,1515569100.0,1515570000.0,29,15,9.696e-05,0.00010133413533834584,false,"roi"],["ETH/BTC",-0.0,1515569700.0,1515573300.0,31,60,0.0943,0.09477268170426063,false,"roi"],["XMR/BTC",0.00997506,1515570000.0,1515571800.0,32,30,0.02719607,0.02760503345864661,false,"roi"],["ZEC/BTC",0.0,1515572100.0,1515578100.0,39,100,0.04634952,0.046581848421052625,false,"roi"],["NXT/BTC",-0.0,1515595500.0,1515599400.0,117,65,3.066e-05,3.081368421052631e-05,false,"roi"],["LTC/BTC",0.0,1515602100.0,1515604500.0,139,40,0.0168999,0.016984611278195488,false,"roi"],["ETH/BTC",-0.0,1515602400.0,1515604800.0,140,40,0.09132568,0.0917834528320802,false,"roi"],["ETH/BTC",-0.0,1515610200.0,1515613500.0,166,55,0.08898003,0.08942604518796991,false,"roi"],["ETH/BTC",0.0,1515622500.0,1515625200.0,207,45,0.08560008,0.08602915308270676,false,"roi"],["ETC/BTC",0.00997506,1515624600.0,1515626400.0,214,30,0.00249083,0.0025282860902255634,false,"roi"],["NXT/BTC",-0.0,1515626100.0,1515629700.0,219,60,3.022e-05,3.037147869674185e-05,false,"roi"],["ETC/BTC",0.01995012,1515627600.0,1515629100.0,224,25,0.002437,0.0024980776942355883,false,"roi"],["ZEC/BTC",0.00997506,1515628800.0,1515630900.0,228,35,0.04771803,0.04843559436090225,false,"roi"],["XLM/BTC",-0.10448878,1515642000.0,1515644700.0,272,45,3.651e-05,3.2859000000000005e-05,false,"stop_loss"],["ETH/BTC",0.00997506,1515642900.0,1515644700.0,275,30,0.08824105,0.08956798308270676,false,"roi"],["ETC/BTC",-0.0,1515643200.0,1515646200.0,276,50,0.00243,0.002442180451127819,false,"roi"],["ZEC/BTC",0.01995012,1515645000.0,1515646500.0,282,25,0.04545064,0.046589753784461146,false,"roi"],["XLM/BTC",0.01995012,1515645000.0,1515646200.0,282,20,3.372e-05,3.456511278195488e-05,false,"roi"],["XMR/BTC",0.01995012,1515646500.0,1515647700.0,287,20,0.02644,0.02710265664160401,false,"roi"],["ETH/BTC",-0.0,1515669600.0,1515672000.0,364,40,0.08812,0.08856170426065162,false,"roi"],["XMR/BTC",-0.0,1515670500.0,1515672900.0,367,40,0.02683577,0.026970285137844607,false,"roi"],["ADA/BTC",0.01995012,1515679200.0,1515680700.0,396,25,4.919e-05,5.04228320802005e-05,false,"roi"],["ETH/BTC",-0.0,1515698700.0,1515702900.0,461,70,0.08784896,0.08828930566416039,false,"roi"],["ADA/BTC",-0.0,1515710100.0,1515713400.0,499,55,5.105e-05,5.130588972431077e-05,false,"roi"],["XLM/BTC",0.00997506,1515711300.0,1515713100.0,503,30,3.96e-05,4.019548872180451e-05,false,"roi"],["NXT/BTC",-0.0,1515711300.0,1515713700.0,503,40,2.885e-05,2.899461152882205e-05,false,"roi"],["XMR/BTC",0.00997506,1515713400.0,1515715500.0,510,35,0.02645,0.026847744360902256,false,"roi"],["ZEC/BTC",-0.0,1515714900.0,1515719700.0,515,80,0.048,0.04824060150375939,false,"roi"],["XLM/BTC",0.01995012,1515791700.0,1515793200.0,771,25,4.692e-05,4.809593984962405e-05,false,"roi"],["ETC/BTC",-0.0,1515804900.0,1515824400.0,815,325,0.00256966,0.0025825405012531327,false,"roi"],["ADA/BTC",0.0,1515840900.0,1515843300.0,935,40,6.262e-05,6.293388471177944e-05,false,"roi"],["XLM/BTC",0.0,1515848700.0,1516025400.0,961,2945,4.73e-05,4.753709273182957e-05,false,"roi"],["ADA/BTC",-0.0,1515850200.0,1515854700.0,966,75,6.063e-05,6.0933909774436085e-05,false,"roi"],["POWR/BTC",-0.0,1515850800.0,1515886200.0,968,590,0.00011082,0.00011137548872180449,false,"roi"],["ADA/BTC",-0.0,1515856500.0,1515858900.0,987,40,5.93e-05,5.9597243107769415e-05,false,"roi"],["ZEC/BTC",-0.0,1515861000.0,1515863400.0,1002,40,0.04850003,0.04874313791979949,false,"roi"],["ETH/BTC",-0.0,1515881100.0,1515911100.0,1069,500,0.09825019,0.09874267215538847,false,"roi"],["ADA/BTC",0.0,1515889200.0,1515970500.0,1096,1355,6.018e-05,6.048165413533834e-05,false,"roi"],["ETH/BTC",-0.0,1515933900.0,1515936300.0,1245,40,0.09758999,0.0980791628822055,false,"roi"],["ETC/BTC",0.00997506,1515943800.0,1515945600.0,1278,30,0.00311,0.0031567669172932328,false,"roi"],["ETC/BTC",-0.0,1515962700.0,1515968100.0,1341,90,0.00312401,0.003139669197994987,false,"roi"],["LTC/BTC",0.0,1515972900.0,1515976200.0,1375,55,0.0174679,0.017555458395989976,false,"roi"],["DASH/BTC",-0.0,1515973500.0,1515975900.0,1377,40,0.07346846,0.07383672295739348,false,"roi"],["ETH/BTC",-0.0,1515983100.0,1515985500.0,1409,40,0.097994,0.09848519799498745,false,"roi"],["ETH/BTC",-0.0,1516000800.0,1516003200.0,1468,40,0.09659,0.09707416040100249,false,"roi"],["POWR/BTC",0.00997506,1516004400.0,1516006500.0,1480,35,9.987e-05,0.00010137180451127818,false,"roi"],["ETH/BTC",0.0,1516018200.0,1516071000.0,1526,880,0.0948969,0.09537257368421052,false,"roi"],["DASH/BTC",-0.0,1516025400.0,1516038000.0,1550,210,0.071,0.07135588972431077,false,"roi"],["ZEC/BTC",-0.0,1516026600.0,1516029000.0,1554,40,0.04600501,0.046235611553884705,false,"roi"],["POWR/BTC",-0.0,1516039800.0,1516044300.0,1598,75,9.438e-05,9.485308270676691e-05,false,"roi"],["XMR/BTC",-0.0,1516041300.0,1516043700.0,1603,40,0.03040001,0.030552391002506264,false,"roi"],["ADA/BTC",-0.10448878,1516047900.0,1516091100.0,1625,720,5.837e-05,5.2533e-05,false,"stop_loss"],["ZEC/BTC",-0.0,1516048800.0,1516053600.0,1628,80,0.046036,0.04626675689223057,false,"roi"],["ETC/BTC",-0.0,1516062600.0,1516065000.0,1674,40,0.0028685,0.0028828784461152877,false,"roi"],["DASH/BTC",0.0,1516065300.0,1516070100.0,1683,80,0.06731755,0.0676549813283208,false,"roi"],["ETH/BTC",0.0,1516088700.0,1516092000.0,1761,55,0.09217614,0.09263817578947368,false,"roi"],["LTC/BTC",0.01995012,1516091700.0,1516092900.0,1771,20,0.0165,0.016913533834586467,false,"roi"],["POWR/BTC",0.03990025,1516091700.0,1516092000.0,1771,5,7.953e-05,8.311781954887218e-05,false,"roi"],["ZEC/BTC",-0.0,1516092300.0,1516096200.0,1773,65,0.045202,0.04542857644110275,false,"roi"],["ADA/BTC",0.00997506,1516094100.0,1516095900.0,1779,30,5.248e-05,5.326917293233082e-05,false,"roi"],["XMR/BTC",0.0,1516094100.0,1516096500.0,1779,40,0.02892318,0.02906815834586466,false,"roi"],["ADA/BTC",0.01995012,1516096200.0,1516097400.0,1786,20,5.158e-05,5.287273182957392e-05,false,"roi"],["ZEC/BTC",0.00997506,1516097100.0,1516099200.0,1789,35,0.04357584,0.044231115789473675,false,"roi"],["XMR/BTC",0.00997506,1516097100.0,1516098900.0,1789,30,0.02828232,0.02870761804511278,false,"roi"],["ADA/BTC",0.00997506,1516110300.0,1516112400.0,1833,35,5.362e-05,5.4426315789473676e-05,false,"roi"],["ADA/BTC",-0.0,1516123800.0,1516127100.0,1878,55,5.302e-05,5.328576441102756e-05,false,"roi"],["ETH/BTC",0.00997506,1516126500.0,1516128300.0,1887,30,0.09129999,0.09267292218045112,false,"roi"],["XLM/BTC",0.01995012,1516126500.0,1516127700.0,1887,20,3.808e-05,3.903438596491228e-05,false,"roi"],["XMR/BTC",0.00997506,1516129200.0,1516131000.0,1896,30,0.02811012,0.028532828571428567,false,"roi"],["ETC/BTC",-0.10448878,1516137900.0,1516141500.0,1925,60,0.00258379,0.002325411,false,"stop_loss"],["NXT/BTC",-0.10448878,1516137900.0,1516142700.0,1925,80,2.559e-05,2.3031e-05,false,"stop_loss"],["POWR/BTC",-0.10448878,1516138500.0,1516141500.0,1927,50,7.62e-05,6.858e-05,false,"stop_loss"],["LTC/BTC",0.03990025,1516141800.0,1516142400.0,1938,10,0.0151,0.015781203007518795,false,"roi"],["ETC/BTC",0.03990025,1516141800.0,1516142100.0,1938,5,0.00229844,0.002402129022556391,false,"roi"],["ETC/BTC",0.03990025,1516142400.0,1516142700.0,1940,5,0.00235676,0.00246308,false,"roi"],["DASH/BTC",0.01995012,1516142700.0,1516143900.0,1941,20,0.0630692,0.06464988170426066,false,"roi"],["NXT/BTC",0.03990025,1516143000.0,1516143300.0,1942,5,2.2e-05,2.2992481203007514e-05,false,"roi"],["ADA/BTC",0.00997506,1516159800.0,1516161600.0,1998,30,4.974e-05,5.048796992481203e-05,false,"roi"],["POWR/BTC",0.01995012,1516161300.0,1516162500.0,2003,20,7.108e-05,7.28614536340852e-05,false,"roi"],["ZEC/BTC",-0.0,1516181700.0,1516184100.0,2071,40,0.04327,0.04348689223057644,false,"roi"],["ADA/BTC",-0.0,1516184400.0,1516208400.0,2080,400,4.997e-05,5.022047619047618e-05,false,"roi"],["DASH/BTC",-0.0,1516185000.0,1516188300.0,2082,55,0.06836818,0.06871087764411027,false,"roi"],["XLM/BTC",-0.0,1516185000.0,1516187400.0,2082,40,3.63e-05,3.648195488721804e-05,false,"roi"],["XMR/BTC",-0.0,1516192200.0,1516226700.0,2106,575,0.0281,0.02824085213032581,false,"roi"],["ETH/BTC",-0.0,1516192500.0,1516208100.0,2107,260,0.08651001,0.08694364413533832,false,"roi"],["ADA/BTC",-0.0,1516251600.0,1516254900.0,2304,55,5.633e-05,5.6612355889724306e-05,false,"roi"],["DASH/BTC",0.00997506,1516252800.0,1516254900.0,2308,35,0.06988494,0.07093584135338346,false,"roi"],["ADA/BTC",-0.0,1516260900.0,1516263300.0,2335,40,5.545e-05,5.572794486215538e-05,false,"roi"],["LTC/BTC",-0.0,1516266000.0,1516268400.0,2352,40,0.01633527,0.016417151052631574,false,"roi"],["ETC/BTC",-0.0,1516293600.0,1516296000.0,2444,40,0.00269734,0.0027108605012531326,false,"roi"],["XLM/BTC",0.01995012,1516298700.0,1516300200.0,2461,25,4.475e-05,4.587155388471177e-05,false,"roi"],["NXT/BTC",0.00997506,1516299900.0,1516301700.0,2465,30,2.79e-05,2.8319548872180444e-05,false,"roi"],["ZEC/BTC",0.0,1516306200.0,1516308600.0,2486,40,0.04439326,0.04461578260651629,false,"roi"],["XLM/BTC",0.0,1516311000.0,1516322100.0,2502,185,4.49e-05,4.51250626566416e-05,false,"roi"],["XMR/BTC",-0.0,1516312500.0,1516338300.0,2507,430,0.02855,0.028693107769423555,false,"roi"],["ADA/BTC",0.0,1516313400.0,1516315800.0,2510,40,5.796e-05,5.8250526315789473e-05,false,"roi"],["ZEC/BTC",0.0,1516319400.0,1516321800.0,2530,40,0.04340323,0.04362079005012531,false,"roi"],["ZEC/BTC",0.0,1516380300.0,1516383300.0,2733,50,0.04454455,0.04476783095238095,false,"roi"],["ADA/BTC",-0.0,1516382100.0,1516391700.0,2739,160,5.62e-05,5.648170426065162e-05,false,"roi"],["XLM/BTC",-0.0,1516382400.0,1516392900.0,2740,175,4.339e-05,4.360749373433584e-05,false,"roi"],["POWR/BTC",0.0,1516423500.0,1516469700.0,2877,770,0.0001009,0.00010140576441102757,false,"roi"],["ETC/BTC",-0.0,1516423800.0,1516461300.0,2878,625,0.00270505,0.002718609147869674,false,"roi"],["XMR/BTC",-0.0,1516423800.0,1516431600.0,2878,130,0.03000002,0.030150396040100245,false,"roi"],["ADA/BTC",-0.0,1516438800.0,1516441200.0,2928,40,5.46e-05,5.4873684210526304e-05,false,"roi"],["XMR/BTC",-0.10448878,1516472700.0,1516852200.0,3041,6325,0.03082222,0.027739998000000002,false,"stop_loss"],["ETH/BTC",-0.0,1516487100.0,1516490100.0,3089,50,0.08969999,0.09014961401002504,false,"roi"],["LTC/BTC",0.0,1516503000.0,1516545000.0,3142,700,0.01632501,0.01640683962406015,false,"roi"],["DASH/BTC",-0.0,1516530000.0,1516532400.0,3232,40,0.070538,0.07089157393483708,false,"roi"],["ADA/BTC",-0.0,1516549800.0,1516560300.0,3298,175,5.301e-05,5.3275714285714276e-05,false,"roi"],["XLM/BTC",0.0,1516551600.0,1516554000.0,3304,40,3.955e-05,3.9748245614035085e-05,false,"roi"],["ETC/BTC",0.00997506,1516569300.0,1516571100.0,3363,30,0.00258505,0.002623922932330827,false,"roi"],["XLM/BTC",-0.0,1516569300.0,1516571700.0,3363,40,3.903e-05,3.922563909774435e-05,false,"roi"],["ADA/BTC",-0.0,1516581300.0,1516617300.0,3403,600,5.236e-05,5.262245614035087e-05,false,"roi"],["POWR/BTC",0.0,1516584600.0,1516587000.0,3414,40,9.028e-05,9.073253132832079e-05,false,"roi"],["ETC/BTC",-0.0,1516623900.0,1516631700.0,3545,130,0.002687,0.002700468671679198,false,"roi"],["XLM/BTC",-0.0,1516626900.0,1516629300.0,3555,40,4.168e-05,4.1888922305764405e-05,false,"roi"],["POWR/BTC",0.00997506,1516629600.0,1516631400.0,3564,30,8.821e-05,8.953646616541353e-05,false,"roi"],["ADA/BTC",-0.0,1516636500.0,1516639200.0,3587,45,5.172e-05,5.1979248120300745e-05,false,"roi"],["NXT/BTC",0.01995012,1516637100.0,1516638300.0,3589,20,3.026e-05,3.101839598997494e-05,false,"roi"],["DASH/BTC",0.0,1516650600.0,1516666200.0,3634,260,0.07064,0.07099408521303258,false,"roi"],["LTC/BTC",0.0,1516656300.0,1516658700.0,3653,40,0.01644483,0.01652726022556391,false,"roi"],["XLM/BTC",0.00997506,1516665900.0,1516667700.0,3685,30,4.331e-05,4.3961278195488714e-05,false,"roi"],["NXT/BTC",0.01995012,1516672200.0,1516673700.0,3706,25,3.2e-05,3.2802005012531326e-05,false,"roi"],["ETH/BTC",0.0,1516681500.0,1516684500.0,3737,50,0.09167706,0.09213659413533835,false,"roi"],["DASH/BTC",0.0,1516692900.0,1516698000.0,3775,85,0.0692498,0.06959691679197995,false,"roi"],["NXT/BTC",0.0,1516704600.0,1516712700.0,3814,135,3.182e-05,3.197949874686716e-05,false,"roi"],["ZEC/BTC",-0.0,1516705500.0,1516723500.0,3817,300,0.04088,0.04108491228070175,false,"roi"],["ADA/BTC",-0.0,1516719300.0,1516721700.0,3863,40,5.15e-05,5.175814536340851e-05,false,"roi"],["ETH/BTC",0.0,1516725300.0,1516752300.0,3883,450,0.09071698,0.09117170170426064,false,"roi"],["NXT/BTC",-0.0,1516728300.0,1516733100.0,3893,80,3.128e-05,3.1436791979949865e-05,false,"roi"],["POWR/BTC",-0.0,1516738500.0,1516744800.0,3927,105,9.555e-05,9.602894736842104e-05,false,"roi"],["ZEC/BTC",-0.0,1516746600.0,1516749000.0,3954,40,0.04080001,0.041004521328320796,false,"roi"],["ADA/BTC",-0.0,1516751400.0,1516764900.0,3970,225,5.163e-05,5.1888796992481196e-05,false,"roi"],["ZEC/BTC",0.0,1516753200.0,1516758600.0,3976,90,0.04040781,0.04061035541353383,false,"roi"],["ADA/BTC",-0.0,1516776300.0,1516778700.0,4053,40,5.132e-05,5.157724310776942e-05,false,"roi"],["ADA/BTC",0.03990025,1516803300.0,1516803900.0,4143,10,5.198e-05,5.432496240601503e-05,false,"roi"],["NXT/BTC",-0.0,1516805400.0,1516811700.0,4150,105,3.054e-05,3.069308270676692e-05,false,"roi"],["POWR/BTC",0.0,1516806600.0,1516810500.0,4154,65,9.263e-05,9.309431077694235e-05,false,"roi"],["ADA/BTC",-0.0,1516833600.0,1516836300.0,4244,45,5.514e-05,5.5416390977443596e-05,false,"roi"],["XLM/BTC",0.0,1516841400.0,1516843800.0,4270,40,4.921e-05,4.9456666666666664e-05,false,"roi"],["ETC/BTC",0.0,1516868100.0,1516882500.0,4359,240,0.0026,0.002613032581453634,false,"roi"],["XMR/BTC",-0.0,1516875900.0,1516896900.0,4385,350,0.02799871,0.028139054411027563,false,"roi"],["ZEC/BTC",-0.0,1516878000.0,1516880700.0,4392,45,0.04078902,0.0409934762406015,false,"roi"],["NXT/BTC",-0.0,1516885500.0,1516887900.0,4417,40,2.89e-05,2.904486215538847e-05,false,"roi"],["ZEC/BTC",-0.0,1516886400.0,1516889100.0,4420,45,0.041103,0.041309030075187964,false,"roi"],["XLM/BTC",0.00997506,1516895100.0,1516896900.0,4449,30,5.428e-05,5.5096240601503756e-05,false,"roi"],["XLM/BTC",-0.0,1516902300.0,1516922100.0,4473,330,5.414e-05,5.441137844611528e-05,false,"roi"],["ZEC/BTC",-0.0,1516914900.0,1516917300.0,4515,40,0.04140777,0.0416153277443609,false,"roi"],["ETC/BTC",0.0,1516932300.0,1516934700.0,4573,40,0.00254309,0.002555837318295739,false,"roi"],["ADA/BTC",-0.0,1516935300.0,1516979400.0,4583,735,5.607e-05,5.6351052631578935e-05,false,"roi"],["ETC/BTC",0.0,1516947000.0,1516958700.0,4622,195,0.00253806,0.0025507821052631577,false,"roi"],["ZEC/BTC",-0.0,1516951500.0,1516960500.0,4637,150,0.0415,0.04170802005012531,false,"roi"],["XLM/BTC",0.00997506,1516960500.0,1516962300.0,4667,30,5.321e-05,5.401015037593984e-05,false,"roi"],["XMR/BTC",-0.0,1516982700.0,1516985100.0,4741,40,0.02772046,0.02785940967418546,false,"roi"],["ETH/BTC",0.0,1517009700.0,1517012100.0,4831,40,0.09461341,0.09508766268170425,false,"roi"],["XLM/BTC",-0.0,1517013300.0,1517016600.0,4843,55,5.615e-05,5.643145363408521e-05,false,"roi"],["ADA/BTC",-0.07877175,1517013900.0,1517287500.0,4845,4560,5.556e-05,5.144e-05,true,"force_sell"],["DASH/BTC",-0.0,1517020200.0,1517052300.0,4866,535,0.06900001,0.06934587471177944,false,"roi"],["ETH/BTC",-0.0,1517034300.0,1517036700.0,4913,40,0.09449985,0.09497353345864659,false,"roi"],["ZEC/BTC",-0.04815133,1517046000.0,1517287200.0,4952,4020,0.0410697,0.03928809,true,"force_sell"],["XMR/BTC",-0.0,1517053500.0,1517056200.0,4977,45,0.0285,0.02864285714285714,false,"roi"],["XMR/BTC",-0.0,1517056500.0,1517066700.0,4987,170,0.02866372,0.02880739779448621,false,"roi"],["ETH/BTC",-0.0,1517068200.0,1517071800.0,5026,60,0.095381,0.09585910025062655,false,"roi"],["DASH/BTC",-0.0,1517072700.0,1517075100.0,5041,40,0.06759092,0.06792972160401002,false,"roi"],["ETC/BTC",-0.0,1517096400.0,1517101500.0,5120,85,0.00258501,0.002597967443609022,false,"roi"],["DASH/BTC",-0.0,1517106300.0,1517127000.0,5153,345,0.06698502,0.0673207845112782,false,"roi"],["DASH/BTC",-0.0,1517135100.0,1517157000.0,5249,365,0.0677177,0.06805713709273183,false,"roi"],["XLM/BTC",0.0,1517171700.0,1517175300.0,5371,60,5.215e-05,5.2411403508771925e-05,false,"roi"],["ETC/BTC",0.00997506,1517176800.0,1517178600.0,5388,30,0.00273809,0.002779264285714285,false,"roi"],["ETC/BTC",0.00997506,1517184000.0,1517185800.0,5412,30,0.00274632,0.002787618045112782,false,"roi"],["LTC/BTC",0.0,1517192100.0,1517194800.0,5439,45,0.01622478,0.016306107218045113,false,"roi"],["DASH/BTC",-0.0,1517195100.0,1517197500.0,5449,40,0.069,0.06934586466165413,false,"roi"],["POWR/BTC",-0.0,1517203200.0,1517208900.0,5476,95,8.755e-05,8.798884711779448e-05,false,"roi"],["DASH/BTC",-0.0,1517209200.0,1517253900.0,5496,745,0.06825763,0.06859977350877192,false,"roi"],["DASH/BTC",-0.0,1517255100.0,1517257500.0,5649,40,0.06713892,0.06747545593984962,false,"roi"],["POWR/BTC",-0.0199116,1517268600.0,1517287500.0,5694,315,8.934e-05,8.8e-05,true,"force_sell"]] \ No newline at end of file +[["TRX/BTC",0.03990025,1515568500.0,1515568800.0,27,5,9.64e-05,0.00010074887218045112,false,"roi"],["ADA/BTC",0.03990025,1515568500.0,1515569400.0,27,15,4.756e-05,4.9705563909774425e-05,false,"roi"],["XLM/BTC",0.03990025,1515569100.0,1515569700.0,29,10,3.339e-05,3.489631578947368e-05,false,"roi"],["TRX/BTC",0.03990025,1515569100.0,1515570000.0,29,15,9.696e-05,0.00010133413533834584,false,"roi"],["ETH/BTC",-0.0,1515569700.0,1515573300.0,31,60,0.0943,0.09477268170426063,false,"roi"],["XMR/BTC",0.00997506,1515570000.0,1515571800.0,32,30,0.02719607,0.02760503345864661,false,"roi"],["ZEC/BTC",0.0,1515572100.0,1515578100.0,39,100,0.04634952,0.046581848421052625,false,"roi"],["NXT/BTC",-0.0,1515595500.0,1515599400.0,117,65,3.066e-05,3.081368421052631e-05,false,"roi"],["LTC/BTC",0.0,1515602100.0,1515604500.0,139,40,0.0168999,0.016984611278195488,false,"roi"],["ETH/BTC",-0.0,1515602400.0,1515604800.0,140,40,0.09132568,0.0917834528320802,false,"roi"],["ETH/BTC",-0.0,1515610200.0,1515613500.0,166,55,0.08898003,0.08942604518796991,false,"roi"],["ETH/BTC",0.0,1515622500.0,1515625200.0,207,45,0.08560008,0.08602915308270676,false,"roi"],["ETC/BTC",0.00997506,1515624600.0,1515626400.0,214,30,0.00249083,0.0025282860902255634,false,"roi"],["NXT/BTC",-0.0,1515626100.0,1515629700.0,219,60,3.022e-05,3.037147869674185e-05,false,"roi"],["ETC/BTC",0.01995012,1515627600.0,1515629100.0,224,25,0.002437,0.0024980776942355883,false,"roi"],["ZEC/BTC",0.00997506,1515628800.0,1515630900.0,228,35,0.04771803,0.04843559436090225,false,"roi"],["XLM/BTC",-0.10448878,1515642000.0,1515644700.0,272,45,3.651e-05,3.2859000000000005e-05,false,"stop_loss"],["ETH/BTC",0.00997506,1515642900.0,1515644700.0,275,30,0.08824105,0.08956798308270676,false,"roi"],["ETC/BTC",-0.0,1515643200.0,1515646200.0,276,50,0.00243,0.002442180451127819,false,"roi"],["ZEC/BTC",0.01995012,1515645000.0,1515646500.0,282,25,0.04545064,0.046589753784461146,false,"roi"],["XLM/BTC",0.01995012,1515645000.0,1515646200.0,282,20,3.372e-05,3.456511278195488e-05,false,"roi"],["XMR/BTC",0.01995012,1515646500.0,1515647700.0,287,20,0.02644,0.02710265664160401,false,"roi"],["ETH/BTC",-0.0,1515669600.0,1515672000.0,364,40,0.08812,0.08856170426065162,false,"roi"],["XMR/BTC",-0.0,1515670500.0,1515672900.0,367,40,0.02683577,0.026970285137844607,false,"roi"],["ADA/BTC",0.01995012,1515679200.0,1515680700.0,396,25,4.919e-05,5.04228320802005e-05,false,"roi"],["ETH/BTC",-0.0,1515698700.0,1515702900.0,461,70,0.08784896,0.08828930566416039,false,"roi"],["ADA/BTC",-0.0,1515710100.0,1515713400.0,499,55,5.105e-05,5.130588972431077e-05,false,"roi"],["XLM/BTC",0.00997506,1515711300.0,1515713100.0,503,30,3.96e-05,4.019548872180451e-05,false,"roi"],["NXT/BTC",-0.0,1515711300.0,1515713700.0,503,40,2.885e-05,2.899461152882205e-05,false,"roi"],["XMR/BTC",0.00997506,1515713400.0,1515715500.0,510,35,0.02645,0.026847744360902256,false,"roi"],["ZEC/BTC",-0.0,1515714900.0,1515719700.0,515,80,0.048,0.04824060150375939,false,"roi"],["XLM/BTC",0.01995012,1515791700.0,1515793200.0,771,25,4.692e-05,4.809593984962405e-05,false,"roi"],["ETC/BTC",-0.0,1515804900.0,1515824400.0,815,325,0.00256966,0.0025825405012531327,false,"roi"],["ADA/BTC",0.0,1515840900.0,1515843300.0,935,40,6.262e-05,6.293388471177944e-05,false,"roi"],["XLM/BTC",0.0,1515848700.0,1516025400.0,961,2945,4.73e-05,4.753709273182957e-05,false,"roi"],["ADA/BTC",-0.0,1515850200.0,1515854700.0,966,75,6.063e-05,6.0933909774436085e-05,false,"roi"],["TRX/BTC",-0.0,1515850800.0,1515886200.0,968,590,0.00011082,0.00011137548872180449,false,"roi"],["ADA/BTC",-0.0,1515856500.0,1515858900.0,987,40,5.93e-05,5.9597243107769415e-05,false,"roi"],["ZEC/BTC",-0.0,1515861000.0,1515863400.0,1002,40,0.04850003,0.04874313791979949,false,"roi"],["ETH/BTC",-0.0,1515881100.0,1515911100.0,1069,500,0.09825019,0.09874267215538847,false,"roi"],["ADA/BTC",0.0,1515889200.0,1515970500.0,1096,1355,6.018e-05,6.048165413533834e-05,false,"roi"],["ETH/BTC",-0.0,1515933900.0,1515936300.0,1245,40,0.09758999,0.0980791628822055,false,"roi"],["ETC/BTC",0.00997506,1515943800.0,1515945600.0,1278,30,0.00311,0.0031567669172932328,false,"roi"],["ETC/BTC",-0.0,1515962700.0,1515968100.0,1341,90,0.00312401,0.003139669197994987,false,"roi"],["LTC/BTC",0.0,1515972900.0,1515976200.0,1375,55,0.0174679,0.017555458395989976,false,"roi"],["DASH/BTC",-0.0,1515973500.0,1515975900.0,1377,40,0.07346846,0.07383672295739348,false,"roi"],["ETH/BTC",-0.0,1515983100.0,1515985500.0,1409,40,0.097994,0.09848519799498745,false,"roi"],["ETH/BTC",-0.0,1516000800.0,1516003200.0,1468,40,0.09659,0.09707416040100249,false,"roi"],["TRX/BTC",0.00997506,1516004400.0,1516006500.0,1480,35,9.987e-05,0.00010137180451127818,false,"roi"],["ETH/BTC",0.0,1516018200.0,1516071000.0,1526,880,0.0948969,0.09537257368421052,false,"roi"],["DASH/BTC",-0.0,1516025400.0,1516038000.0,1550,210,0.071,0.07135588972431077,false,"roi"],["ZEC/BTC",-0.0,1516026600.0,1516029000.0,1554,40,0.04600501,0.046235611553884705,false,"roi"],["TRX/BTC",-0.0,1516039800.0,1516044300.0,1598,75,9.438e-05,9.485308270676691e-05,false,"roi"],["XMR/BTC",-0.0,1516041300.0,1516043700.0,1603,40,0.03040001,0.030552391002506264,false,"roi"],["ADA/BTC",-0.10448878,1516047900.0,1516091100.0,1625,720,5.837e-05,5.2533e-05,false,"stop_loss"],["ZEC/BTC",-0.0,1516048800.0,1516053600.0,1628,80,0.046036,0.04626675689223057,false,"roi"],["ETC/BTC",-0.0,1516062600.0,1516065000.0,1674,40,0.0028685,0.0028828784461152877,false,"roi"],["DASH/BTC",0.0,1516065300.0,1516070100.0,1683,80,0.06731755,0.0676549813283208,false,"roi"],["ETH/BTC",0.0,1516088700.0,1516092000.0,1761,55,0.09217614,0.09263817578947368,false,"roi"],["LTC/BTC",0.01995012,1516091700.0,1516092900.0,1771,20,0.0165,0.016913533834586467,false,"roi"],["TRX/BTC",0.03990025,1516091700.0,1516092000.0,1771,5,7.953e-05,8.311781954887218e-05,false,"roi"],["ZEC/BTC",-0.0,1516092300.0,1516096200.0,1773,65,0.045202,0.04542857644110275,false,"roi"],["ADA/BTC",0.00997506,1516094100.0,1516095900.0,1779,30,5.248e-05,5.326917293233082e-05,false,"roi"],["XMR/BTC",0.0,1516094100.0,1516096500.0,1779,40,0.02892318,0.02906815834586466,false,"roi"],["ADA/BTC",0.01995012,1516096200.0,1516097400.0,1786,20,5.158e-05,5.287273182957392e-05,false,"roi"],["ZEC/BTC",0.00997506,1516097100.0,1516099200.0,1789,35,0.04357584,0.044231115789473675,false,"roi"],["XMR/BTC",0.00997506,1516097100.0,1516098900.0,1789,30,0.02828232,0.02870761804511278,false,"roi"],["ADA/BTC",0.00997506,1516110300.0,1516112400.0,1833,35,5.362e-05,5.4426315789473676e-05,false,"roi"],["ADA/BTC",-0.0,1516123800.0,1516127100.0,1878,55,5.302e-05,5.328576441102756e-05,false,"roi"],["ETH/BTC",0.00997506,1516126500.0,1516128300.0,1887,30,0.09129999,0.09267292218045112,false,"roi"],["XLM/BTC",0.01995012,1516126500.0,1516127700.0,1887,20,3.808e-05,3.903438596491228e-05,false,"roi"],["XMR/BTC",0.00997506,1516129200.0,1516131000.0,1896,30,0.02811012,0.028532828571428567,false,"roi"],["ETC/BTC",-0.10448878,1516137900.0,1516141500.0,1925,60,0.00258379,0.002325411,false,"stop_loss"],["NXT/BTC",-0.10448878,1516137900.0,1516142700.0,1925,80,2.559e-05,2.3031e-05,false,"stop_loss"],["TRX/BTC",-0.10448878,1516138500.0,1516141500.0,1927,50,7.62e-05,6.858e-05,false,"stop_loss"],["LTC/BTC",0.03990025,1516141800.0,1516142400.0,1938,10,0.0151,0.015781203007518795,false,"roi"],["ETC/BTC",0.03990025,1516141800.0,1516142100.0,1938,5,0.00229844,0.002402129022556391,false,"roi"],["ETC/BTC",0.03990025,1516142400.0,1516142700.0,1940,5,0.00235676,0.00246308,false,"roi"],["DASH/BTC",0.01995012,1516142700.0,1516143900.0,1941,20,0.0630692,0.06464988170426066,false,"roi"],["NXT/BTC",0.03990025,1516143000.0,1516143300.0,1942,5,2.2e-05,2.2992481203007514e-05,false,"roi"],["ADA/BTC",0.00997506,1516159800.0,1516161600.0,1998,30,4.974e-05,5.048796992481203e-05,false,"roi"],["TRX/BTC",0.01995012,1516161300.0,1516162500.0,2003,20,7.108e-05,7.28614536340852e-05,false,"roi"],["ZEC/BTC",-0.0,1516181700.0,1516184100.0,2071,40,0.04327,0.04348689223057644,false,"roi"],["ADA/BTC",-0.0,1516184400.0,1516208400.0,2080,400,4.997e-05,5.022047619047618e-05,false,"roi"],["DASH/BTC",-0.0,1516185000.0,1516188300.0,2082,55,0.06836818,0.06871087764411027,false,"roi"],["XLM/BTC",-0.0,1516185000.0,1516187400.0,2082,40,3.63e-05,3.648195488721804e-05,false,"roi"],["XMR/BTC",-0.0,1516192200.0,1516226700.0,2106,575,0.0281,0.02824085213032581,false,"roi"],["ETH/BTC",-0.0,1516192500.0,1516208100.0,2107,260,0.08651001,0.08694364413533832,false,"roi"],["ADA/BTC",-0.0,1516251600.0,1516254900.0,2304,55,5.633e-05,5.6612355889724306e-05,false,"roi"],["DASH/BTC",0.00997506,1516252800.0,1516254900.0,2308,35,0.06988494,0.07093584135338346,false,"roi"],["ADA/BTC",-0.0,1516260900.0,1516263300.0,2335,40,5.545e-05,5.572794486215538e-05,false,"roi"],["LTC/BTC",-0.0,1516266000.0,1516268400.0,2352,40,0.01633527,0.016417151052631574,false,"roi"],["ETC/BTC",-0.0,1516293600.0,1516296000.0,2444,40,0.00269734,0.0027108605012531326,false,"roi"],["XLM/BTC",0.01995012,1516298700.0,1516300200.0,2461,25,4.475e-05,4.587155388471177e-05,false,"roi"],["NXT/BTC",0.00997506,1516299900.0,1516301700.0,2465,30,2.79e-05,2.8319548872180444e-05,false,"roi"],["ZEC/BTC",0.0,1516306200.0,1516308600.0,2486,40,0.04439326,0.04461578260651629,false,"roi"],["XLM/BTC",0.0,1516311000.0,1516322100.0,2502,185,4.49e-05,4.51250626566416e-05,false,"roi"],["XMR/BTC",-0.0,1516312500.0,1516338300.0,2507,430,0.02855,0.028693107769423555,false,"roi"],["ADA/BTC",0.0,1516313400.0,1516315800.0,2510,40,5.796e-05,5.8250526315789473e-05,false,"roi"],["ZEC/BTC",0.0,1516319400.0,1516321800.0,2530,40,0.04340323,0.04362079005012531,false,"roi"],["ZEC/BTC",0.0,1516380300.0,1516383300.0,2733,50,0.04454455,0.04476783095238095,false,"roi"],["ADA/BTC",-0.0,1516382100.0,1516391700.0,2739,160,5.62e-05,5.648170426065162e-05,false,"roi"],["XLM/BTC",-0.0,1516382400.0,1516392900.0,2740,175,4.339e-05,4.360749373433584e-05,false,"roi"],["TRX/BTC",0.0,1516423500.0,1516469700.0,2877,770,0.0001009,0.00010140576441102757,false,"roi"],["ETC/BTC",-0.0,1516423800.0,1516461300.0,2878,625,0.00270505,0.002718609147869674,false,"roi"],["XMR/BTC",-0.0,1516423800.0,1516431600.0,2878,130,0.03000002,0.030150396040100245,false,"roi"],["ADA/BTC",-0.0,1516438800.0,1516441200.0,2928,40,5.46e-05,5.4873684210526304e-05,false,"roi"],["XMR/BTC",-0.10448878,1516472700.0,1516852200.0,3041,6325,0.03082222,0.027739998000000002,false,"stop_loss"],["ETH/BTC",-0.0,1516487100.0,1516490100.0,3089,50,0.08969999,0.09014961401002504,false,"roi"],["LTC/BTC",0.0,1516503000.0,1516545000.0,3142,700,0.01632501,0.01640683962406015,false,"roi"],["DASH/BTC",-0.0,1516530000.0,1516532400.0,3232,40,0.070538,0.07089157393483708,false,"roi"],["ADA/BTC",-0.0,1516549800.0,1516560300.0,3298,175,5.301e-05,5.3275714285714276e-05,false,"roi"],["XLM/BTC",0.0,1516551600.0,1516554000.0,3304,40,3.955e-05,3.9748245614035085e-05,false,"roi"],["ETC/BTC",0.00997506,1516569300.0,1516571100.0,3363,30,0.00258505,0.002623922932330827,false,"roi"],["XLM/BTC",-0.0,1516569300.0,1516571700.0,3363,40,3.903e-05,3.922563909774435e-05,false,"roi"],["ADA/BTC",-0.0,1516581300.0,1516617300.0,3403,600,5.236e-05,5.262245614035087e-05,false,"roi"],["TRX/BTC",0.0,1516584600.0,1516587000.0,3414,40,9.028e-05,9.073253132832079e-05,false,"roi"],["ETC/BTC",-0.0,1516623900.0,1516631700.0,3545,130,0.002687,0.002700468671679198,false,"roi"],["XLM/BTC",-0.0,1516626900.0,1516629300.0,3555,40,4.168e-05,4.1888922305764405e-05,false,"roi"],["TRX/BTC",0.00997506,1516629600.0,1516631400.0,3564,30,8.821e-05,8.953646616541353e-05,false,"roi"],["ADA/BTC",-0.0,1516636500.0,1516639200.0,3587,45,5.172e-05,5.1979248120300745e-05,false,"roi"],["NXT/BTC",0.01995012,1516637100.0,1516638300.0,3589,20,3.026e-05,3.101839598997494e-05,false,"roi"],["DASH/BTC",0.0,1516650600.0,1516666200.0,3634,260,0.07064,0.07099408521303258,false,"roi"],["LTC/BTC",0.0,1516656300.0,1516658700.0,3653,40,0.01644483,0.01652726022556391,false,"roi"],["XLM/BTC",0.00997506,1516665900.0,1516667700.0,3685,30,4.331e-05,4.3961278195488714e-05,false,"roi"],["NXT/BTC",0.01995012,1516672200.0,1516673700.0,3706,25,3.2e-05,3.2802005012531326e-05,false,"roi"],["ETH/BTC",0.0,1516681500.0,1516684500.0,3737,50,0.09167706,0.09213659413533835,false,"roi"],["DASH/BTC",0.0,1516692900.0,1516698000.0,3775,85,0.0692498,0.06959691679197995,false,"roi"],["NXT/BTC",0.0,1516704600.0,1516712700.0,3814,135,3.182e-05,3.197949874686716e-05,false,"roi"],["ZEC/BTC",-0.0,1516705500.0,1516723500.0,3817,300,0.04088,0.04108491228070175,false,"roi"],["ADA/BTC",-0.0,1516719300.0,1516721700.0,3863,40,5.15e-05,5.175814536340851e-05,false,"roi"],["ETH/BTC",0.0,1516725300.0,1516752300.0,3883,450,0.09071698,0.09117170170426064,false,"roi"],["NXT/BTC",-0.0,1516728300.0,1516733100.0,3893,80,3.128e-05,3.1436791979949865e-05,false,"roi"],["TRX/BTC",-0.0,1516738500.0,1516744800.0,3927,105,9.555e-05,9.602894736842104e-05,false,"roi"],["ZEC/BTC",-0.0,1516746600.0,1516749000.0,3954,40,0.04080001,0.041004521328320796,false,"roi"],["ADA/BTC",-0.0,1516751400.0,1516764900.0,3970,225,5.163e-05,5.1888796992481196e-05,false,"roi"],["ZEC/BTC",0.0,1516753200.0,1516758600.0,3976,90,0.04040781,0.04061035541353383,false,"roi"],["ADA/BTC",-0.0,1516776300.0,1516778700.0,4053,40,5.132e-05,5.157724310776942e-05,false,"roi"],["ADA/BTC",0.03990025,1516803300.0,1516803900.0,4143,10,5.198e-05,5.432496240601503e-05,false,"roi"],["NXT/BTC",-0.0,1516805400.0,1516811700.0,4150,105,3.054e-05,3.069308270676692e-05,false,"roi"],["TRX/BTC",0.0,1516806600.0,1516810500.0,4154,65,9.263e-05,9.309431077694235e-05,false,"roi"],["ADA/BTC",-0.0,1516833600.0,1516836300.0,4244,45,5.514e-05,5.5416390977443596e-05,false,"roi"],["XLM/BTC",0.0,1516841400.0,1516843800.0,4270,40,4.921e-05,4.9456666666666664e-05,false,"roi"],["ETC/BTC",0.0,1516868100.0,1516882500.0,4359,240,0.0026,0.002613032581453634,false,"roi"],["XMR/BTC",-0.0,1516875900.0,1516896900.0,4385,350,0.02799871,0.028139054411027563,false,"roi"],["ZEC/BTC",-0.0,1516878000.0,1516880700.0,4392,45,0.04078902,0.0409934762406015,false,"roi"],["NXT/BTC",-0.0,1516885500.0,1516887900.0,4417,40,2.89e-05,2.904486215538847e-05,false,"roi"],["ZEC/BTC",-0.0,1516886400.0,1516889100.0,4420,45,0.041103,0.041309030075187964,false,"roi"],["XLM/BTC",0.00997506,1516895100.0,1516896900.0,4449,30,5.428e-05,5.5096240601503756e-05,false,"roi"],["XLM/BTC",-0.0,1516902300.0,1516922100.0,4473,330,5.414e-05,5.441137844611528e-05,false,"roi"],["ZEC/BTC",-0.0,1516914900.0,1516917300.0,4515,40,0.04140777,0.0416153277443609,false,"roi"],["ETC/BTC",0.0,1516932300.0,1516934700.0,4573,40,0.00254309,0.002555837318295739,false,"roi"],["ADA/BTC",-0.0,1516935300.0,1516979400.0,4583,735,5.607e-05,5.6351052631578935e-05,false,"roi"],["ETC/BTC",0.0,1516947000.0,1516958700.0,4622,195,0.00253806,0.0025507821052631577,false,"roi"],["ZEC/BTC",-0.0,1516951500.0,1516960500.0,4637,150,0.0415,0.04170802005012531,false,"roi"],["XLM/BTC",0.00997506,1516960500.0,1516962300.0,4667,30,5.321e-05,5.401015037593984e-05,false,"roi"],["XMR/BTC",-0.0,1516982700.0,1516985100.0,4741,40,0.02772046,0.02785940967418546,false,"roi"],["ETH/BTC",0.0,1517009700.0,1517012100.0,4831,40,0.09461341,0.09508766268170425,false,"roi"],["XLM/BTC",-0.0,1517013300.0,1517016600.0,4843,55,5.615e-05,5.643145363408521e-05,false,"roi"],["ADA/BTC",-0.07877175,1517013900.0,1517287500.0,4845,4560,5.556e-05,5.144e-05,true,"force_sell"],["DASH/BTC",-0.0,1517020200.0,1517052300.0,4866,535,0.06900001,0.06934587471177944,false,"roi"],["ETH/BTC",-0.0,1517034300.0,1517036700.0,4913,40,0.09449985,0.09497353345864659,false,"roi"],["ZEC/BTC",-0.04815133,1517046000.0,1517287200.0,4952,4020,0.0410697,0.03928809,true,"force_sell"],["XMR/BTC",-0.0,1517053500.0,1517056200.0,4977,45,0.0285,0.02864285714285714,false,"roi"],["XMR/BTC",-0.0,1517056500.0,1517066700.0,4987,170,0.02866372,0.02880739779448621,false,"roi"],["ETH/BTC",-0.0,1517068200.0,1517071800.0,5026,60,0.095381,0.09585910025062655,false,"roi"],["DASH/BTC",-0.0,1517072700.0,1517075100.0,5041,40,0.06759092,0.06792972160401002,false,"roi"],["ETC/BTC",-0.0,1517096400.0,1517101500.0,5120,85,0.00258501,0.002597967443609022,false,"roi"],["DASH/BTC",-0.0,1517106300.0,1517127000.0,5153,345,0.06698502,0.0673207845112782,false,"roi"],["DASH/BTC",-0.0,1517135100.0,1517157000.0,5249,365,0.0677177,0.06805713709273183,false,"roi"],["XLM/BTC",0.0,1517171700.0,1517175300.0,5371,60,5.215e-05,5.2411403508771925e-05,false,"roi"],["ETC/BTC",0.00997506,1517176800.0,1517178600.0,5388,30,0.00273809,0.002779264285714285,false,"roi"],["ETC/BTC",0.00997506,1517184000.0,1517185800.0,5412,30,0.00274632,0.002787618045112782,false,"roi"],["LTC/BTC",0.0,1517192100.0,1517194800.0,5439,45,0.01622478,0.016306107218045113,false,"roi"],["DASH/BTC",-0.0,1517195100.0,1517197500.0,5449,40,0.069,0.06934586466165413,false,"roi"],["TRX/BTC",-0.0,1517203200.0,1517208900.0,5476,95,8.755e-05,8.798884711779448e-05,false,"roi"],["DASH/BTC",-0.0,1517209200.0,1517253900.0,5496,745,0.06825763,0.06859977350877192,false,"roi"],["DASH/BTC",-0.0,1517255100.0,1517257500.0,5649,40,0.06713892,0.06747545593984962,false,"roi"],["TRX/BTC",-0.0199116,1517268600.0,1517287500.0,5694,315,8.934e-05,8.8e-05,true,"force_sell"]] diff --git a/tests/testdata/pairs.json b/tests/testdata/pairs.json index f4bab6dc5..15aae2643 100644 --- a/tests/testdata/pairs.json +++ b/tests/testdata/pairs.json @@ -9,7 +9,7 @@ "LTC/BTC", "NEO/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "STORJ/BTC", "QTUM/BTC", "WAVES/BTC", From f20f5cebbe24f53660c0161b8556249d4ce0ba19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 11:09:41 +0100 Subject: [PATCH 081/319] Move performance-calculation to persistence --- freqtrade/persistence.py | 23 ++++++++++++++++++++--- freqtrade/rpc/rpc.py | 13 +------------ tests/test_persistence.py | 24 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 1850aafd9..6bd4b0a30 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -8,17 +8,15 @@ from typing import Any, Dict, List, Optional import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, - create_engine, inspect) + create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker -from sqlalchemy import func from sqlalchemy.pool import StaticPool from freqtrade import OperationalException - logger = logging.getLogger(__name__) @@ -404,6 +402,25 @@ class Trade(_DECL_BASE): .scalar() return total_open_stake_amount or 0 + @staticmethod + def get_overall_performance() -> Dict: + pair_rates = Trade.session.query( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum')) \ + .all() + return [ + { + 'pair': pair, + 'profit': round(rate * 100, 2), + 'count': count + } + for pair, rate, count in pair_rates + ] + @staticmethod def get_open_trades() -> List[Any]: """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f994ac006..c50a7937e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -442,18 +442,7 @@ class RPC: Handler for performance. Shows a performance statistic from finished trades """ - - pair_rates = Trade.session.query(Trade.pair, - sql.func.sum(Trade.close_profit).label('profit_sum'), - sql.func.count(Trade.pair).label('count')) \ - .filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(sql.text('profit_sum DESC')) \ - .all() - return [ - {'pair': pair, 'profit': round(rate * 100, 2), 'count': count} - for pair, rate, count in pair_rates - ] + return Trade.get_overall_performance() def _rpc_count(self) -> Dict[str, float]: """ Returns the number of trades running """ diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6bd223a9b..8cf5f1756 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -35,6 +35,8 @@ def create_mock_trades(fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, + close_rate=0.128, + close_profit=0.005, exchange='bittrex', is_open=False, open_order_id='dry_run_sell_12345' @@ -835,3 +837,25 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade_adj.stop_loss_pct == -0.04 assert trade_adj.initial_stop_loss == 0.96 assert trade_adj.initial_stop_loss_pct == -0.04 + + +@pytest.mark.usefixtures("init_persistence") +def test_total_open_trades_stakes(fee): + + res = Trade.total_open_trades_stakes() + assert res == 0 + create_mock_trades(fee) + res = Trade.total_open_trades_stakes() + assert res == 0.002 + + +@pytest.mark.usefixtures("init_persistence") +def test_get_overall_performance(fee): + + create_mock_trades(fee) + res = Trade.get_overall_performance() + + assert len(res) == 1 + assert 'pair' in res[0] + assert 'profit' in res[0] + assert 'count' in res[0] From ab117527c9a89d68056023982ff0129c8fe71605 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 11:15:33 +0100 Subject: [PATCH 082/319] Refactor get_best_pair to persistence --- freqtrade/persistence.py | 9 +++++++++ freqtrade/rpc/rpc.py | 6 +----- tests/test_persistence.py | 13 +++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 6bd4b0a30..fe0b64bcc 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -421,6 +421,15 @@ class Trade(_DECL_BASE): for pair, rate, count in pair_rates ] + @staticmethod + def get_best_pair(): + best_pair = Trade.session.query( + Trade.pair, func.sum(Trade.close_profit).label('profit_sum') + ).filter(Trade.is_open.is_(False)) \ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum')).first() + return best_pair + @staticmethod def get_open_trades() -> List[Any]: """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c50a7937e..dc25c3743 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -225,11 +225,7 @@ class RPC: ) profit_all_perc.append(profit_percent) - best_pair = Trade.session.query( - Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum') - ).filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(sql.text('profit_sum DESC')).first() + best_pair = Trade.get_best_pair() if not best_pair: raise RPCException('no closed trade') diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 8cf5f1756..4aa69423e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -859,3 +859,16 @@ def test_get_overall_performance(fee): assert 'pair' in res[0] assert 'profit' in res[0] assert 'count' in res[0] + + +@pytest.mark.usefixtures("init_persistence") +def test_get_best_pair(fee): + + res = Trade.get_best_pair() + assert res is None + + create_mock_trades(fee) + res = Trade.get_best_pair() + assert len(res) == 2 + assert res[0] == 'ETC/BTC' + assert res[1] == 0.005 From 01efebc42f27ae0250955d84bc99868ed0877a2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 13:32:07 +0100 Subject: [PATCH 083/319] Extract query to it's own function --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a8fc6bc7e..ef9a85154 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -768,7 +768,7 @@ class FreqtradeBot: buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime - for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): + for trade in Trade.get_open_order_trades(): try: # FIXME: Somehow the query above returns results # where the open_order_id is in fact None. diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index fe0b64bcc..e527cde16 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -391,6 +391,13 @@ class Trade(_DECL_BASE): profit_percent = (close_trade_price / open_trade_price) - 1 return float(f"{profit_percent:.8f}") + @staticmethod + def get_open_order_trades(): + """ + Returns all open trades + """ + return Trade.query.filter(Trade.open_order_id.isnot(None)).all() + @staticmethod def total_open_trades_stakes() -> float: """ @@ -403,7 +410,10 @@ class Trade(_DECL_BASE): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance() -> Dict: + def get_overall_performance() -> List[Dict]: + """ + Returns List of dicts containing all Trades, including profit and trade count + """ pair_rates = Trade.session.query( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), @@ -423,6 +433,9 @@ class Trade(_DECL_BASE): @staticmethod def get_best_pair(): + """ + Get best pair with closed trade. + """ best_pair = Trade.session.query( Trade.pair, func.sum(Trade.close_profit).label('profit_sum') ).filter(Trade.is_open.is_(False)) \ From 26a5800a7f9afbd8236e2d89d6c26c29556f1268 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 15:01:10 +0100 Subject: [PATCH 084/319] Extract get_trades function --- freqtrade/data/btanalysis.py | 2 +- freqtrade/persistence.py | 31 +++++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 0f5d395ff..462547d9e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -106,7 +106,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: t.stop_loss, t.initial_stop_loss, t.strategy, t.ticker_interval ) - for t in Trade.query.all()], + for t in Trade.get_trades().all()], columns=columns) return trades diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index e527cde16..808e42c9a 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -11,6 +11,7 @@ from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Query from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool @@ -391,12 +392,33 @@ class Trade(_DECL_BASE): profit_percent = (close_trade_price / open_trade_price) - 1 return float(f"{profit_percent:.8f}") + @staticmethod + def get_trades(trade_filter=None) -> Query: + """ + Helper function to query Trades using filter. + :param trade_filter: Filter to apply to trades + :return: Query object + """ + if trade_filter is not None: + if not isinstance(trade_filter, list): + trade_filter = [trade_filter] + return Trade.query.filter(*trade_filter) + else: + return Trade.query + + @staticmethod + def get_open_trades() -> List[Any]: + """ + Query trades from persistence layer + """ + return Trade.get_trades(Trade.is_open.is_(True)).all() + @staticmethod def get_open_order_trades(): """ Returns all open trades """ - return Trade.query.filter(Trade.open_order_id.isnot(None)).all() + return Trade.get_trades(Trade.open_order_id.isnot(None)).all() @staticmethod def total_open_trades_stakes() -> float: @@ -443,13 +465,6 @@ class Trade(_DECL_BASE): .order_by(desc('profit_sum')).first() return best_pair - @staticmethod - def get_open_trades() -> List[Any]: - """ - Query trades from persistence layer - """ - return Trade.query.filter(Trade.is_open.is_(True)).all() - @staticmethod def stoploss_reinitialization(desired_stoploss): """ From b37c5e4878c1900f8cb34d68438a59cf2c5c987d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 15:09:01 +0100 Subject: [PATCH 085/319] use get_trades in rpc modules --- freqtrade/persistence.py | 9 ++++++--- freqtrade/rpc/rpc.py | 25 ++++++++++--------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 808e42c9a..a15db87c7 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -395,9 +395,12 @@ class Trade(_DECL_BASE): @staticmethod def get_trades(trade_filter=None) -> Query: """ - Helper function to query Trades using filter. - :param trade_filter: Filter to apply to trades - :return: Query object + Helper function to query Trades using filters. + :param trade_filter: Optional filter to apply to trades + Can be either a Filter object, or a List of filters + e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])` + e.g. `(trade_filter=Trade.id == trade_id)` + :return: unsorted query object """ if trade_filter is not None: if not isinstance(trade_filter, list): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index dc25c3743..8eecb04f9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -9,7 +9,6 @@ from enum import Enum from typing import Dict, Any, List, Optional import arrow -import sqlalchemy as sql from numpy import mean, NAN from pandas import DataFrame @@ -154,12 +153,11 @@ class RPC: for day in range(0, timescale): profitday = today - timedelta(days=day) - trades = Trade.query \ - .filter(Trade.is_open.is_(False)) \ - .filter(Trade.close_date >= profitday)\ - .filter(Trade.close_date < (profitday + timedelta(days=1)))\ - .order_by(Trade.close_date)\ - .all() + trades = Trade.get_trades(trade_filter=[ + Trade.is_open.is_(False), + Trade.close_date >= profitday, + Trade.close_date < (profitday + timedelta(days=1)) + ]).order_by(Trade.close_date).all() curdayprofit = sum(trade.calc_profit() for trade in trades) profit_days[profitday] = { 'amount': f'{curdayprofit:.8f}', @@ -192,7 +190,7 @@ class RPC: def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: """ Returns cumulative profit statistics """ - trades = Trade.query.order_by(Trade.id).all() + trades = Trade.get_trades().order_by(Trade.id).all() profit_all_coin = [] profit_all_perc = [] @@ -385,11 +383,8 @@ class RPC: return {'result': 'Created sell orders for all open trades.'} # Query for trade - trade = Trade.query.filter( - sql.and_( - Trade.id == trade_id, - Trade.is_open.is_(True) - ) + trade = Trade.get_trades( + trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] ).first() if not trade: logger.warning('forcesell: Invalid argument received') @@ -419,7 +414,7 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() if trade: raise RPCException(f'position for {pair} already open - id: {trade.id}') @@ -428,7 +423,7 @@ class RPC: # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): - trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() return trade else: return None From c2076d86a4f32901784f905f251d3b94bd1cc1a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 14:26:03 +0100 Subject: [PATCH 086/319] Use scoped_session as intended --- freqtrade/persistence.py | 8 +++++--- tests/test_persistence.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index a15db87c7..27a283378 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -51,9 +51,11 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: raise OperationalException(f"Given value for db_url: '{db_url}' " f"is no valid database URL! (See {_SQL_DOCS_URL})") - session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) - Trade.session = session() - Trade.query = session.query_property() + # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope + # Scoped sessions proxy requests to the appropriate thread-local session. + # We should use the scoped_session object - not a seperately initialized version + Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) + Trade.query = Trade.session.query_property() _DECL_BASE.metadata.create_all(engine) check_migrate(engine) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4aa69423e..231a1d2e2 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -61,7 +61,7 @@ def test_init_create_session(default_conf): # Check if init create a session init(default_conf['db_url'], default_conf['dry_run']) assert hasattr(Trade, 'session') - assert 'Session' in type(Trade.session).__name__ + assert 'scoped_session' in type(Trade.session).__name__ def test_init_custom_db_url(default_conf, mocker): From 5ed777114837cd549b17193df6a37b3e64c41792 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Oct 2019 15:39:36 +0100 Subject: [PATCH 087/319] Update documentation to include get_trades fixes #1753 --- docs/strategy-customization.md | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index cef362ffd..71d6f3e8f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -405,6 +405,52 @@ if self.wallets: - `get_used(asset)` - currently tied up balance (open orders) - `get_total(asset)` - total available balance - sum of the 2 above +### Additional data (Trades) + +A history of Trades can be retrieved in the strategy by querying the database. + +At the top of the file, import Trade. + +```python +from freqtrade.persistence import Trade +``` + +The following example queries for the current pair and trades from today, however other filters easily be added. + +``` python +if self.config['runmode'] in ('live', 'dry_run'): + trades = Trade.get_trades([Trade.pair == metadata['pair'], + Trade.open_date > datetime.utcnow() - timedelta(days=1), + Trade.is_open == False, + ]).order_by(Trade.close_date).all() + # Summarize profit for this pair. + curdayprofit = sum(trade.close_profit for trade in trades) +``` + +Get amount of stake_currency currently invested in Trades: + +``` python +if self.config['runmode'] in ('live', 'dry_run'): + total_stakes = Trade.total_open_trades_stakes() +``` + +Retrieve performance per pair. +Returns a List of dicts per pair. + +``` python +if self.config['runmode'] in ('live', 'dry_run'): + performance = Trade.get_overall_performance() +``` + +Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5%. + +``` json +{'pair': "ETH/BTC", 'profit': 1.5, 'count': 5} +``` + +!!! Warning + Trade history is not available during backtesting or hyperopt. + ### Print created dataframe To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`. From b7b1e66c6ee3c6dc4342aef73dc5c4e7c8056701 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 09:59:54 +0100 Subject: [PATCH 088/319] Convert to % as part of RPC to allow users to use unrounded ratio --- docs/strategy-customization.md | 6 +++--- freqtrade/persistence.py | 4 ++-- freqtrade/rpc/rpc.py | 7 +++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 71d6f3e8f..b3b6e3548 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -415,7 +415,7 @@ At the top of the file, import Trade. from freqtrade.persistence import Trade ``` -The following example queries for the current pair and trades from today, however other filters easily be added. +The following example queries for the current pair and trades from today, however other filters can easily be added. ``` python if self.config['runmode'] in ('live', 'dry_run'): @@ -442,10 +442,10 @@ if self.config['runmode'] in ('live', 'dry_run'): performance = Trade.get_overall_performance() ``` -Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5%. +Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015). ``` json -{'pair': "ETH/BTC", 'profit': 1.5, 'count': 5} +{'pair': "ETH/BTC", 'profit': 0.015, 'count': 5} ``` !!! Warning diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 27a283378..735c740c3 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -437,7 +437,7 @@ class Trade(_DECL_BASE): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance() -> List[Dict]: + def get_overall_performance() -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count """ @@ -452,7 +452,7 @@ class Trade(_DECL_BASE): return [ { 'pair': pair, - 'profit': round(rate * 100, 2), + 'profit': rate, 'count': count } for pair, rate, count in pair_rates diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8eecb04f9..4aed48f74 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -428,12 +428,15 @@ class RPC: else: return None - def _rpc_performance(self) -> List[Dict]: + def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. Shows a performance statistic from finished trades """ - return Trade.get_overall_performance() + pair_rates = Trade.get_overall_performance() + # Round and convert to % + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates] + return pair_rates def _rpc_count(self) -> Dict[str, float]: """ Returns the number of trades running """ From 7a96d3c9ae8992d2962640e668be1d6bebbb5a72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 13:27:04 +0100 Subject: [PATCH 089/319] Update raspbian install documentation Fix "box" titles ... they need to be in quotes! --- docs/configuration.md | 2 +- docs/data-download.md | 2 +- docs/docker.md | 2 +- docs/installation.md | 25 +++++++++++-------------- docs/plotting.md | 2 +- docs/rest-api.md | 8 ++++---- docs/strategy-customization.md | 8 ++++---- docs/telegram-usage.md | 2 +- 8 files changed, 24 insertions(+), 27 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1ad13c87a..6ba0447b5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -254,7 +254,7 @@ Configuration: !!! Note If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order. -!!! Warning stoploss_on_exchange failures +!!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. ### Understand order_time_in_force diff --git a/docs/data-download.md b/docs/data-download.md index bf4792ea3..f105e7a56 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -8,7 +8,7 @@ If no additional parameter is specified, freqtrade will download data for `"1m"` Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. -!!! Tip Updating existing data +!!! Tip "Tip: Updating existing data" If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. diff --git a/docs/docker.md b/docs/docker.md index 923dec1e2..8a254b749 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -26,7 +26,7 @@ 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 +!!! 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`. diff --git a/docs/installation.md b/docs/installation.md index 02870a1c1..50f24d3e8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -97,27 +97,24 @@ sudo apt-get install build-essential git #### Raspberry Pi / Raspbian -Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/). +The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. +This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. -The following assumes that miniconda3 is installed and available in your environment. Since the last miniconda3 installation file uses python 3.4, we will update to python 3.6 on this installation. -It's recommended to use (mini)conda for this as installation/compilation of `numpy` and `pandas` takes a long time. - -Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). +Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. ``` bash -conda config --add channels rpi -conda install python=3.6 -conda create -n freqtrade python=3.6 -conda activate freqtrade -conda install pandas numpy +sudo apt-get install python3-venv libatlas-base-dev +git clone https://github.com/freqtrade/freqtrade.git +cd freqtrade -sudo apt install libffi-dev -python3 -m pip install -r requirements-common.txt -python3 -m pip install -e . +bash setup.sh -i ``` +!!! Note "Installation duration" + Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. + !!! Note - This does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. + The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. ### Common diff --git a/docs/plotting.md b/docs/plotting.md index 89404f8b1..ca56bdaba 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -79,7 +79,7 @@ The `-p/--pairs` argument can be used to specify pairs you would like to plot. Specify custom indicators. Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices). -!!! tip +!!! Tip You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command. ``` bash diff --git a/docs/rest-api.md b/docs/rest-api.md index efcacc8a1..4d5bc5730 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -16,11 +16,11 @@ Sample configuration: }, ``` -!!! Danger Security warning - By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. +!!! Danger "Security warning" + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. -!!! Danger Password selection - Please make sure to select a very strong, unique password to protect your bot from unauthorized access. +!!! Danger "Password selection" + Please make sure to select a very strong, unique password to protect your bot from unauthorized access. You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index cef362ffd..8836f4f88 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -51,13 +51,13 @@ freqtrade --strategy AwesomeStrategy **For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py) file as reference.** -!!! Note Strategies and Backtesting +!!! Note "Strategies and Backtesting" To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware that during backtesting the full time-interval is passed to the `populate_*()` methods at once. It is therefore best to use vectorized operations (across the whole dataframe, not loops) and avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle. -!!! Warning Using future data +!!! Warning "Warning: Using future data" Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author needs to take care to avoid having the strategy utilize data from the future. Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document. @@ -330,12 +330,12 @@ if self.dp: ticker_interval=inf_timeframe) ``` -!!! Warning Warning about backtesting +!!! Warning "Warning about backtesting" 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 +!!! Warning "Warning in hyperopt" This option cannot currently be used during hyperopt. #### Orderbook diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index e06d4fdfc..424b55faf 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -93,7 +93,7 @@ Once all positions are sold, run `/stop` to completely stop the bot. `/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. -!!! warning +!!! Warning The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. ### /status From 78fe5a46c1c8d18836740eddc77f478fc3dadad2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 13:27:36 +0100 Subject: [PATCH 090/319] Update travis to verify for correct title usage --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 405228ab8..eb171521d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,8 @@ jobs: - script: # Test Documentation boxes - # !!! : is not allowed! - - grep -Er '^!{3}\s\S+:' docs/*; test $? -ne 0 + # !!! "title" - Title needs to be quoted! + - grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*; test $? -ne 0 name: doc syntax - script: mypy freqtrade scripts name: mypy From dac88c6aedf8582ea608df7471720cc83e015727 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 13:35:55 +0100 Subject: [PATCH 091/319] extract Find parallel trades per interval --- freqtrade/data/btanalysis.py | 32 ++++++++++++++++++++++-------- tests/optimize/test_backtesting.py | 6 +++--- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 0f5d395ff..9dbd69e3e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -52,16 +52,17 @@ def load_backtest_data(filename) -> pd.DataFrame: return df -def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int) -> pd.DataFrame: +def parallel_trade_analysis(results: pd.DataFrame, timeframe: str) -> pd.DataFrame: """ Find overlapping trades by expanding each trade once per period it was open - and then counting overlaps + and then counting overlaps. :param results: Results Dataframe - can be loaded - :param freq: Frequency used for the backtest - :param max_open_trades: parameter max_open_trades used during backtest run - :return: dataframe with open-counts per time-period in freq + :param timeframe: Timeframe used for backtest + :return: dataframe with open-counts per time-period in timeframe """ - dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq)) + from freqtrade.exchange import timeframe_to_minutes + timeframe_min = timeframe_to_minutes(timeframe) + dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=f"{timeframe_min}min")) for row in results[['open_time', 'close_time']].iterrows()] deltas = [len(x) for x in dates] dates = pd.Series(pd.concat(dates).values, name='date') @@ -69,8 +70,23 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int df2 = pd.concat([dates, df2], axis=1) df2 = df2.set_index('date') - df_final = df2.resample(freq)[['pair']].count() - return df_final[df_final['pair'] > max_open_trades] + df_final = df2.resample(f"{timeframe_min}min")[['pair']].count() + df_final = df_final.rename({'pair': 'open_trades'}, axis=1) + return df_final + + +def evaluate_result_multi(results: pd.DataFrame, timeframe: str, + max_open_trades: int) -> pd.DataFrame: + """ + Find overlapping trades by expanding each trade once per period it was open + and then counting overlaps + :param results: Results Dataframe - can be loaded + :param timeframe: Frequency used for the backtest + :param max_open_trades: parameter max_open_trades used during backtest run + :return: dataframe with open-counts per time-period in freq + """ + df_final = parallel_trade_analysis(results, timeframe) + return df_final[df_final['open_trades'] > max_open_trades] def load_trades_from_db(db_url: str) -> pd.DataFrame: diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index ba87848ec..5912c5489 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -714,9 +714,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) results = backtesting.backtest(backtest_conf) # Make sure we have parallel trades - assert len(evaluate_result_multi(results, '5min', 2)) > 0 + assert len(evaluate_result_multi(results, '5m', 2)) > 0 # make sure we don't have trades with more than configured max_open_trades - assert len(evaluate_result_multi(results, '5min', 3)) == 0 + assert len(evaluate_result_multi(results, '5m', 3)) == 0 backtest_conf = { 'stake_amount': default_conf['stake_amount'], @@ -727,7 +727,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) 'end_date': max_date, } results = backtesting.backtest(backtest_conf) - assert len(evaluate_result_multi(results, '5min', 1)) == 0 + assert len(evaluate_result_multi(results, '5m', 1)) == 0 def test_backtest_record(default_conf, fee, mocker): From dd408aa5d63df6e02884616910da9b9fc7eb1378 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 14:07:23 +0100 Subject: [PATCH 092/319] Add analyze_trade_parallelism analysis function --- freqtrade/data/btanalysis.py | 7 ++++--- tests/data/test_btanalysis.py | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 9dbd69e3e..b9625e745 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -52,7 +52,7 @@ def load_backtest_data(filename) -> pd.DataFrame: return df -def parallel_trade_analysis(results: pd.DataFrame, timeframe: str) -> pd.DataFrame: +def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataFrame: """ Find overlapping trades by expanding each trade once per period it was open and then counting overlaps. @@ -62,7 +62,8 @@ def parallel_trade_analysis(results: pd.DataFrame, timeframe: str) -> pd.DataFra """ from freqtrade.exchange import timeframe_to_minutes timeframe_min = timeframe_to_minutes(timeframe) - dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=f"{timeframe_min}min")) + dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, + freq=f"{timeframe_min}min")) for row in results[['open_time', 'close_time']].iterrows()] deltas = [len(x) for x in dates] dates = pd.Series(pd.concat(dates).values, name='date') @@ -85,7 +86,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str, :param max_open_trades: parameter max_open_trades used during backtest run :return: dataframe with open-counts per time-period in freq """ - df_final = parallel_trade_analysis(results, timeframe) + df_final = analyze_trade_parallelism(results, timeframe) return df_final[df_final['open_trades'] > max_open_trades] diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 78781cffd..b49344bbd 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -10,7 +10,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, extract_trades_of_period, load_backtest_data, load_trades, - load_trades_from_db) + load_trades_from_db, analyze_trade_parallelism) from freqtrade.data.history import load_data, load_pair_history from tests.test_persistence import create_mock_trades @@ -32,7 +32,7 @@ def test_load_backtest_data(testdatadir): @pytest.mark.usefixtures("init_persistence") -def test_load_trades_db(default_conf, fee, mocker): +def test_load_trades_from_db(default_conf, fee, mocker): create_mock_trades(fee) # remove init so it does not init again @@ -84,6 +84,17 @@ def test_extract_trades_of_period(testdatadir): assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime +def test_analyze_trade_parallelism(default_conf, mocker, testdatadir): + filename = testdatadir / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + + res = analyze_trade_parallelism(bt_data, "5m") + assert isinstance(res, DataFrame) + assert 'open_trades' in res.columns + assert res['open_trades'].max() == 3 + assert res['open_trades'].min() == 0 + + def test_load_trades(default_conf, mocker): db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock()) bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock()) From 6928c685a8ee8894b5494974f90ae079dcae9fc1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 14:12:41 +0100 Subject: [PATCH 093/319] Add documentation sample for parallel_trade_analysis --- docs/strategy_analysis_example.md | 16 ++++++++++ .../notebooks/strategy_analysis_example.ipynb | 29 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 49800bbb3..55f1bd908 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -107,6 +107,22 @@ trades = load_trades_from_db("sqlite:///tradesv3.sqlite") trades.groupby("pair")["sell_reason"].value_counts() ``` +## Analyze the loaded trades for trade parallelism +This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`. + +`parallel_trade_analysis()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle. + + +```python +from freqtrade.data.btanalysis import parallel_trade_analysis + +# Analyze the above +parallel_trades = parallel_trade_analysis(trades, '5m') + + +parallel_trades.plot() +``` + ## Plot results Freqtrade offers interactive plotting capabilities based on plotly. diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/user_data/notebooks/strategy_analysis_example.ipynb index b9576e0bb..edb05a7ca 100644 --- a/user_data/notebooks/strategy_analysis_example.ipynb +++ b/user_data/notebooks/strategy_analysis_example.ipynb @@ -68,9 +68,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "# Load strategy using values set above\n", @@ -169,6 +167,31 @@ "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze the loaded trades for trade parallelism\n", + "This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.\n", + "\n", + "`parallel_trade_analysis()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from freqtrade.data.btanalysis import parallel_trade_analysis\n", + "\n", + "# Analyze the above\n", + "parallel_trades = parallel_trade_analysis(trades, '5m')\n", + "\n", + "\n", + "parallel_trades.plot()" + ] + }, { "cell_type": "markdown", "metadata": {}, From 44289e4c586324e1026ad0de03e1ef3c746128bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 15:55:35 +0100 Subject: [PATCH 094/319] Allow not using files from user_dir --- freqtrade/resolvers/iresolver.py | 10 +++++----- freqtrade/resolvers/pairlist_resolver.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 51c4f7dba..3bad42fd9 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -17,13 +17,13 @@ class IResolver: This class contains all the logic to load custom classes """ - def build_search_paths(self, config, current_path: Path, user_subdir: str, + def build_search_paths(self, config, current_path: Path, user_subdir: Optional[str] = None, extra_dir: Optional[str] = None) -> List[Path]: - abs_paths = [ - config['user_data_dir'].joinpath(user_subdir), - current_path, - ] + abs_paths: List[Path] = [current_path] + + if user_subdir: + abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir)) if extra_dir: # Add extra directory to the top of the search paths diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index c2782a219..0f23bb3fd 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -40,7 +40,7 @@ class PairListResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='pairlist', extra_dir=None) + user_subdir=None, extra_dir=None) pairlist = self._load_object(paths=abs_paths, object_type=IPairList, object_name=pairlist_name, kwargs=kwargs) From fd9c02603cfea41876dd562e224e15025c0637a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 15:59:52 +0100 Subject: [PATCH 095/319] Introduce chainable PairlistFilters --- freqtrade/pairlist/IPairList.py | 36 +++++++++-- freqtrade/pairlist/IPairListFilter.py | 18 ++++++ freqtrade/pairlist/LowPriceFilter.py | 48 ++++++++++++++ freqtrade/pairlist/PrecisionFilter.py | 51 +++++++++++++++ freqtrade/pairlist/StaticPairList.py | 2 +- freqtrade/pairlist/VolumePairList.py | 62 +------------------ .../resolvers/pairlistfilter_resolver.py | 53 ++++++++++++++++ 7 files changed, 203 insertions(+), 67 deletions(-) create mode 100644 freqtrade/pairlist/IPairListFilter.py create mode 100644 freqtrade/pairlist/LowPriceFilter.py create mode 100644 freqtrade/pairlist/PrecisionFilter.py create mode 100644 freqtrade/resolvers/pairlistfilter_resolver.py diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 5afb0c4c2..eb6af9d52 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -6,10 +6,11 @@ Provides lists as configured in config.json """ import logging from abc import ABC, abstractmethod -from typing import List +from typing import Dict, List from freqtrade.exchange import market_is_active - +from freqtrade.pairlist.IPairListFilter import IPairListFilter +from freqtrade.resolvers.pairlistfilter_resolver import PairListFilterResolver logger = logging.getLogger(__name__) @@ -21,6 +22,12 @@ class IPairList(ABC): self._config = config self._whitelist = self._config['exchange']['pair_whitelist'] self._blacklist = self._config['exchange'].get('pair_blacklist', []) + self._filters = self._config.get('pairlist', {}).get('filters', {}) + self._pairlistfilters: List[IPairListFilter] = [] + for pl_filter in self._filters.keys(): + self._pairlistfilters.append( + PairListFilterResolver(pl_filter, freqtrade, self._config).pairlistfilter + ) @property def name(self) -> str: @@ -60,7 +67,23 @@ class IPairList(ABC): -> Please overwrite in subclasses """ - def _validate_whitelist(self, whitelist: List[str]) -> List[str]: + def validate_whitelist(self, pairlist: List[str], + tickers: List[Dict] = []) -> List[str]: + """ + Validate pairlist against active markets and blacklist. + Run PairlistFilters if these are configured. + """ + pairlist = self._whitelist_for_active_markets(pairlist) + + if not tickers: + # Refresh tickers if they are not used by the parent Pairlist + tickers = self._freqtrade.exchange.get_tickers() + + for pl_filter in self._pairlistfilters: + pairlist = pl_filter.filter_pairlist(pairlist, tickers) + return pairlist + + def _whitelist_for_active_markets(self, whitelist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary :param whitelist: the sorted list of pairs the user might want to trade @@ -69,7 +92,7 @@ class IPairList(ABC): """ markets = self._freqtrade.exchange.markets - sanitized_whitelist = set() + sanitized_whitelist: List[str] = [] for pair in whitelist: # pair is not in the generated dynamic market, or in the blacklist ... ignore it if (pair in self.blacklist or pair not in markets @@ -83,7 +106,8 @@ class IPairList(ABC): if not market_is_active(market): logger.info(f"Ignoring {pair} from whitelist. Market is not active.") continue - sanitized_whitelist.add(pair) + if pair not in sanitized_whitelist: + sanitized_whitelist.append(pair) # We need to remove pairs that are unknown - return list(sanitized_whitelist) + return sanitized_whitelist diff --git a/freqtrade/pairlist/IPairListFilter.py b/freqtrade/pairlist/IPairListFilter.py new file mode 100644 index 000000000..4b43f0e9f --- /dev/null +++ b/freqtrade/pairlist/IPairListFilter.py @@ -0,0 +1,18 @@ +import logging +from abc import ABC, abstractmethod +from typing import Dict, List + +logger = logging.getLogger(__name__) + + +class IPairListFilter(ABC): + + def __init__(self, freqtrade, config: dict) -> None: + self._freqtrade = freqtrade + self._config = config + + @abstractmethod + def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: + """ + Method doing the filtering + """ diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/LowPriceFilter.py new file mode 100644 index 000000000..499dd0c15 --- /dev/null +++ b/freqtrade/pairlist/LowPriceFilter.py @@ -0,0 +1,48 @@ +import logging +from copy import deepcopy +from typing import Dict, List + +from freqtrade.pairlist.IPairListFilter import IPairListFilter + +logger = logging.getLogger(__name__) + + +class LowPriceFilter(IPairListFilter): + + def __init__(self, freqtrade, config: dict) -> None: + super().__init__(freqtrade, config) + + self._low_price_percent = config['pairlist']['filters']['LowPriceFilter'].get( + 'low_price_percent', 0) + + def _validate_precision_filter_lowprice(self, ticker) -> bool: + """ + Check if if one price-step is > than a certain barrier. + :param ticker: ticker dict as returned from ccxt.load_markets() + :param precision: Precision + :return: True if the pair can stay, false if it should be removed + """ + precision = self._freqtrade.exchange.markets[ticker['symbol']]['precision']['price'] + + compare = ticker['last'] + 1 / pow(10, precision) + changeperc = (compare - ticker['last']) / ticker['last'] + if changeperc > self._low_price_percent: + logger.info(f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%") + return False + return True + + def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: + """ + Method doing the filtering + """ + + # Copy list since we're modifying this list + for p in deepcopy(pairlist): + ticker = [t for t in tickers if t['symbol'] == p][0] + + # Filter out assets which would not allow setting a stoploss + if self._low_price_percent and not self._validate_precision_filter_lowprice(ticker): + pairlist.remove(p) + + return pairlist diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py new file mode 100644 index 000000000..c720b8e61 --- /dev/null +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -0,0 +1,51 @@ +import logging +from copy import deepcopy +from typing import Dict, List + +from freqtrade.pairlist.IPairListFilter import IPairListFilter + +logger = logging.getLogger(__name__) + + +class PrecisionFilter(IPairListFilter): + + def __init__(self, freqtrade, config: dict) -> None: + super().__init__(freqtrade, config) + + def _validate_precision_filter(self, ticker: dict, stoploss: float) -> bool: + """ + Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very + low value pairs. + :param ticker: ticker dict as returned from ccxt.load_markets() + :param stoploss: stoploss value as set in the configuration + (already cleaned to be 1 - stoploss) + :return: True if the pair can stay, false if it should be removed + """ + stop_price = self._freqtrade.get_target_bid(ticker["symbol"], ticker) * stoploss + # Adjust stop-prices to precision + sp = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], stop_price) + stop_gap_price = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], + stop_price * 0.99) + logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") + if sp <= stop_gap_price: + logger.info(f"Removed {ticker['symbol']} from whitelist, " + f"because stop price {sp} would be <= stop limit {stop_gap_price}") + return False + return True + + def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: + """ + Method doing the filtering + """ + if self._freqtrade.strategy.stoploss is not None: + # Precalculate sanitized stoploss value to avoid recalculation for every pair + stoploss = 1 - abs(self._freqtrade.strategy.stoploss) + # Copy list since we're modifying this list + for p in deepcopy(pairlist): + ticker = [t for t in tickers if t['symbol'] == p][0] + # Filter out assets which would not allow setting a stoploss + if (stoploss and not self._validate_precision_filter(ticker, stoploss)): + pairlist.remove(p) + continue + + return pairlist diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index 5896e814a..074652b25 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -27,4 +27,4 @@ class StaticPairList(IPairList): """ Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively """ - self._whitelist = self._validate_whitelist(self._config['exchange']['pair_whitelist']) + self._whitelist = self.validate_whitelist(self._config['exchange']['pair_whitelist']) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 7c17c6d57..911bb3bda 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -5,7 +5,6 @@ Provides lists as configured in config.json """ import logging -from copy import deepcopy from typing import List from cachetools import TTLCache, cached @@ -30,7 +29,6 @@ class VolumePairList(IPairList): self._number_pairs = self._whitelistconf['number_assets'] self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume') self._precision_filter = self._whitelistconf.get('precision_filter', True) - self._low_price_percent_filter = self._whitelistconf.get('low_price_percent_filter', None) if not self._freqtrade.exchange.exchange_has('fetchTickers'): raise OperationalException( @@ -60,44 +58,6 @@ class VolumePairList(IPairList): self._whitelist = self._gen_pair_whitelist( self._config['stake_currency'], self._sort_key) - def _validate_precision_filter(self, ticker: dict, stoploss: float) -> bool: - """ - Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very - low value pairs. - :param ticker: ticker dict as returned from ccxt.load_markets() - :param stoploss: stoploss value as set in the configuration - (already cleaned to be 1 - stoploss) - :return: True if the pair can stay, false if it should be removed - """ - stop_price = self._freqtrade.get_target_bid(ticker["symbol"], ticker) * stoploss - # Adjust stop-prices to precision - sp = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], stop_price) - stop_gap_price = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], - stop_price * 0.99) - logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") - if sp <= stop_gap_price: - logger.info(f"Removed {ticker['symbol']} from whitelist, " - f"because stop price {sp} would be <= stop limit {stop_gap_price}") - return False - return True - - def _validate_precision_filter_lowprice(self, ticker) -> bool: - """ - Check if if one price-step is > than a certain barrier. - :param ticker: ticker dict as returned from ccxt.load_markets() - :param precision: Precision - :return: True if the pair can stay, false if it should be removed - """ - precision = self._freqtrade.exchange.markets[ticker['symbol']]['precision']['price'] - - compare = ticker['last'] + 1 / pow(10, precision) - changeperc = (compare - ticker['last']) / ticker['last'] - if changeperc > self._low_price_percent_filter: - logger.info(f"Removed {ticker['symbol']} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%") - return False - return True - @cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: """ @@ -114,26 +74,8 @@ class VolumePairList(IPairList): and v[key] is not None)] sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key]) # Validate whitelist to only have active market pairs - valid_pairs = self._validate_whitelist([s['symbol'] for s in sorted_tickers]) - valid_tickers = [t for t in sorted_tickers if t["symbol"] in valid_pairs] + pairs = self.validate_whitelist([s['symbol'] for s in sorted_tickers], tickers) - stoploss = None - if self._freqtrade.strategy.stoploss is not None: - # Precalculate sanitized stoploss value to avoid recalculation for every pair - stoploss = 1 - abs(self._freqtrade.strategy.stoploss) - - # Copy list since we're modifying this list - for t in deepcopy(valid_tickers): - # Filter out assets which would not allow setting a stoploss - if (stoploss and self._precision_filter - and not self._validate_precision_filter(t, stoploss)): - valid_tickers.remove(t) - continue - if self._low_price_percent_filter and not self._validate_precision_filter_lowprice(t,): - valid_tickers.remove(t) - continue - - pairs = [s['symbol'] for s in valid_tickers] - logger.info(f"Searching pairs: {pairs[:self._number_pairs]}") + logger.info(f"Searching {self._number_pairs} pairs: {pairs[:self._number_pairs]}") return pairs diff --git a/freqtrade/resolvers/pairlistfilter_resolver.py b/freqtrade/resolvers/pairlistfilter_resolver.py new file mode 100644 index 000000000..bf86d1c6c --- /dev/null +++ b/freqtrade/resolvers/pairlistfilter_resolver.py @@ -0,0 +1,53 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom pairlists +""" +import logging +from pathlib import Path + +from freqtrade import OperationalException +from freqtrade.pairlist.IPairListFilter import IPairListFilter +from freqtrade.resolvers import IResolver + +logger = logging.getLogger(__name__) + + +class PairListFilterResolver(IResolver): + """ + This class contains all the logic to load custom PairList class + """ + + __slots__ = ['pairlistfilter'] + + def __init__(self, pairlist_name: str, freqtrade, config: dict) -> None: + """ + Load the custom class from config parameter + :param config: configuration dictionary or None + """ + self.pairlistfilter = self._load_pairlist(pairlist_name, config, + kwargs={'freqtrade': freqtrade, + 'config': config}) + + def _load_pairlist( + self, pairlistfilter_name: str, config: dict, kwargs: dict) -> IPairListFilter: + """ + Search and loads the specified pairlist. + :param pairlistfilter_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 = self.build_search_paths(config, current_path=current_path, + user_subdir=None, extra_dir=None) + + pairlist = self._load_object(paths=abs_paths, object_type=IPairListFilter, + object_name=pairlistfilter_name, kwargs=kwargs) + if pairlist: + return pairlist + raise OperationalException( + f"Impossible to load PairlistFilter '{pairlistfilter_name}'. This class does not exist " + "or contains Python code errors." + ) From 640423c362195224aa62a7287e35f6bfee1a6508 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 16:00:16 +0100 Subject: [PATCH 096/319] Add config samples for chainable pairlist filters --- config_full.json.example | 6 +++-- tests/pairlist/test_pairlist.py | 40 +++++++++++++++++---------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 5935de392..366b075fa 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -55,8 +55,10 @@ "config": { "number_assets": 20, "sort_key": "quoteVolume", - "precision_filter": true, - "low_price_percent_filter": 0 + }, + "filters":{ + "PrecisionFilter": {}, + "LowPriceFilter": {"low_price_percent": 0.01} } }, "exchange": { diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index ac27d1c8e..23c48545e 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -26,7 +26,8 @@ def whitelist_conf(default_conf): 'BLK/BTC' ] default_conf['pairlist'] = {'method': 'StaticPairList', - 'config': {'number_assets': 5} + 'config': {'number_assets': 5}, + 'filters': {}, } return default_conf @@ -113,23 +114,24 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): assert set(whitelist) == set(pairslist) -@pytest.mark.parametrize("precision_filter,low_price_filter,base_currency,key,whitelist_result", [ - (False, 0, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), - (False, 0, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'HOT/BTC', 'FUEL/BTC']), - (False, 0, "USDT", "quoteVolume", ['ETH/USDT']), - (False, 0, "ETH", "quoteVolume", []), - (True, 0, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC", 'FUEL/BTC']), - (True, 0, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC", 'FUEL/BTC']), - (False, 0.03, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'FUEL/BTC']), +@pytest.mark.parametrize("filters,base_currency,key,whitelist_result", [ + ({}, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), + ({}, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'HOT/BTC', 'FUEL/BTC']), + ({}, "USDT", "quoteVolume", ['ETH/USDT']), + ({}, "ETH", "quoteVolume", []), + ({"PrecisionFilter": {}}, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC", 'FUEL/BTC']), + ({"PrecisionFilter": {}}, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC", 'FUEL/BTC']), + ({"LowPriceFilter": {"low_price_percent": 0.03}}, "BTC", + "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'FUEL/BTC']), # Hot is removed by precision_filter, Fuel by low_price_filter. - (True, 0.02, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC']), + ({"PrecisionFilter": {}, "LowPriceFilter": {"low_price_percent": 0.02}}, + "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, - precision_filter, low_price_filter, base_currency, key, - whitelist_result, caplog) -> None: + filters, base_currency, key, whitelist_result, + caplog) -> None: whitelist_conf['pairlist']['method'] = 'VolumePairList' - whitelist_conf['pairlist']['config']['precision_filter'] = precision_filter - whitelist_conf['pairlist']['config']['low_price_percent_filter'] = low_price_filter + whitelist_conf['pairlist']['filters'] = filters mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) @@ -139,11 +141,11 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t freqtrade.config['stake_currency'] = base_currency whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key) assert sorted(whitelist) == sorted(whitelist_result) - if precision_filter: + if 'PrecisionFilter' in filters: assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' r'would be <= stop limit.*', caplog) - if low_price_filter: + if 'LowPriceFilter' in filters: assert log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) @@ -179,15 +181,15 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): (['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "is not compatible with exchange"), # BLK/BTC in blacklist (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") # BTT/BTC is inactive ]) -def test_validate_whitelist(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, - log_message): +def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, + log_message): whitelist_conf['pairlist']['method'] = pairlist mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) caplog.clear() - new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) + new_whitelist = freqtrade.pairlists._whitelist_for_active_markets(whitelist) assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC']) assert log_message in caplog.text From d89a7d523502ed680e50387316cccca28ae5004d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 16:30:47 +0100 Subject: [PATCH 097/319] Document new method to configure filters --- docs/configuration.md | 126 ++++++++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7ccb6840a..39fb4b8ac 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -377,6 +377,82 @@ The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT" ``` +## Pairlists + +Pairlists define the list of pairs that the bot should trade. +There are [`StaticPairList`](#static-pair-list) and dynamic Whitelists available. + +In addition to pairlists, [pairlist filters](#available-pairlist-filters) can be configured, which remove certain assets. +These Filters work with all Pairlist providers and are applied in the sequence they occur. + +### Available Pairlists + +* [`StaticPairList`](#static-pair-list) (default, if not configured differently) +* [`VolumePairList`](#volume-pair-list) + +#### Static Pair List + +By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. Inactive markets and blacklisted pairs are removed from the pair_whitelist. + +It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. + +#### Volume Pair List + +`VolumePairList` selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume` and defaults to `quoteVolume`. + +`VolumePairList` does not consider `pair_whitelist`, but selects the top assets from all available markets (with matching stake-currency) on the exchange. +Pairs in `pair_blacklist` are not considered for `VolumePairList`, even if all other filters would match. + +```json +"pairlist": { + "method": "VolumePairList", + "config": { + "number_assets": 20, + "sort_key": "quoteVolume", + }, +``` + +### Available Pairlist Filters + +* [`PrecisionFilter`](#precision-filter) +* [`LowPriceFilter`](#low-price-pair-filter) + +#### Precision Filter + +Filters low-value coins which would not allow setting a stoploss. + +#### Low Price Pair Filter + +The `LowPriceFilter` allows filtering of pairs where a raise of 1 price unit is below the `low_price_percent` ratio. +This option is disabled by default, and will only apply if set to <> 0. + +Calculation example: +Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. + +These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. + +### Full Pairlist example + +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`LowPriceFilter`](#low-price-pair-filter), filtering all assets where 1 priceunit is > 1%. + +```json +"exchange": { + "pair_whitelist": [], + "pair_blacklist": ["BNB/BTC"] +}, +"pairlist": { + "method": "VolumePairList", + "config": { + "number_assets": 20, + "sort_key": "quoteVolume", + }, + "filters":{ + "PrecisionFilter": {}, + "LowPriceFilter": {"low_price_percent": 0.01} + } + }, +``` + ## Switch to Dry-run mode We recommend starting the bot in the Dry-run mode to see how your bot will @@ -406,49 +482,6 @@ creating trades on the exchange. Once you will be happy with your bot performance running in the Dry-run mode, you can switch it to production mode. -### Dynamic Pairlists - -Dynamic pairlists select pairs for you based on the logic configured. -The bot runs against all pairs (with that stake) on the exchange, and a number of assets -(`number_assets`) is selected based on the selected criteria. - -By default, the `StaticPairList` method is used. -The Pairlist method is configured as `pair_whitelist` parameter under the `exchange` -section of the configuration. - -**Available Pairlist methods:** - -* `StaticPairList` - * It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. -* `VolumePairList` - * It selects `number_assets` top pairs based on `sort_key`, which can be one of -`askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. - * By default, low-value coins that would not allow setting a stop loss are filtered out. (set `precision_filter` parameter to `false` to disable this behaviour). - * `VolumePairList` does not consider `pair_whitelist`, but builds this automatically based the pairlist configuration. - * Pairs in `pair_blacklist` are not considered for VolumePairList, even if all other filters would match. - * `low_price_percent_filter` allows filtering of pairs where a raise of 1 price unit is below the `low_price_percent_filter` ratio. - This option is disabled by default, and will only apply if set to <> 0. - Calculation example: Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. - - -Example: - -```json -"exchange": { - "pair_whitelist": [], - "pair_blacklist": ["BNB/BTC"] -}, -"pairlist": { - "method": "VolumePairList", - "config": { - "number_assets": 20, - "sort_key": "quoteVolume", - "precision_filter": true, - "low_price_percent_filter": 0.05 - } - }, -``` - ## Switch to production mode In production mode, the bot will engage your money. Be careful, since a wrong @@ -479,7 +512,7 @@ you run it in production mode. !!! Note If you have an exchange API key yet, [see our tutorial](/pre-requisite). -### Using proxy with FreqTrade +## Using proxy with FreqTrade To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration. @@ -499,14 +532,13 @@ export HTTPS_PROXY="http://addr:port" freqtrade ``` - -### Embedding Strategies +## Embedding Strategies FreqTrade provides you with with an easy way to embed the strategy into your configuration file. This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field, in your chosen config file. -#### Encoding a string as BASE64 +### Encoding a string as BASE64 This is a quick example, how to generate the BASE64 string in python From 14758dbe107d4a460444719c78b7f9fd5477b5b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 16:32:22 +0100 Subject: [PATCH 098/319] Some small cleanups --- freqtrade/configuration/deprecated_settings.py | 7 +++++++ freqtrade/pairlist/LowPriceFilter.py | 4 ++-- tests/test_configuration.py | 10 ++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index f00b23894..8471028aa 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -57,3 +57,10 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: 'experimental', 'sell_profit_only') process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', 'experimental', 'ignore_roi_if_buy_signal') + + if config.get('pairlist', {}).get('config', {}).get('precision_filter'): + logger.warning( + "DEPRECATED: " + f"Using precision_filter setting is deprecated and has been replaced by" + "PrecisionFilter. Please refer to the docs on configuration details") + config['pairlist'].update({'filters': {'PrecisionFilter': {}}}) diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/LowPriceFilter.py index 499dd0c15..778c9b4e0 100644 --- a/freqtrade/pairlist/LowPriceFilter.py +++ b/freqtrade/pairlist/LowPriceFilter.py @@ -15,7 +15,7 @@ class LowPriceFilter(IPairListFilter): self._low_price_percent = config['pairlist']['filters']['LowPriceFilter'].get( 'low_price_percent', 0) - def _validate_precision_filter_lowprice(self, ticker) -> bool: + def _validate_ticker_lowprice(self, ticker) -> bool: """ Check if if one price-step is > than a certain barrier. :param ticker: ticker dict as returned from ccxt.load_markets() @@ -42,7 +42,7 @@ class LowPriceFilter(IPairListFilter): ticker = [t for t in tickers if t['symbol'] == p][0] # Filter out assets which would not allow setting a stoploss - if self._low_price_percent and not self._validate_precision_filter_lowprice(ticker): + if self._low_price_percent and not self._validate_ticker_lowprice(ticker): pairlist.remove(p) return pairlist diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 545dd5df4..258088925 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -963,6 +963,16 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca assert default_conf[setting[0]][setting[1]] == setting[5] +def test_process_deprecated_setting_precision_filter(mocker, default_conf, caplog): + patched_configuration_load_config_file(mocker, default_conf) + default_conf.update({'pairlist': { + 'config': {'precision_filter': True} + }}) + + process_temporary_deprecated_settings(default_conf) + assert log_has_re(r'DEPRECATED.*precision_filter.*', caplog) + + def test_check_conflicting_settings(mocker, default_conf, caplog): patched_configuration_load_config_file(mocker, default_conf) From ad98d61939f9808b0adffb0b06099fdef241eee2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 16:39:45 +0100 Subject: [PATCH 099/319] 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 391493b09..346578c2e 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -127,7 +127,7 @@ This is called with each iteration of the bot - so consider implementing caching Assign the resulting whiteslist to `self._whitelist` and `self._blacklist` respectively. These will then be used to run the bot in this iteration. Pairs with open trades will be added to the whitelist to have the sell-methods run correctly. -Please also run `self._validate_whitelist(pairs)` and to check and remove pairs with inactive markets. This function is available in the Parent class (`StaticPairList`) and should ideally not be overwritten. +Please also run `self.validate_whitelist(pairs, tickers)` (tickers is optional, but should be passed when you're using tickers anyway) and to check and remove pairs with inactive markets. This function is available in the Parent class (`StaticPairList`) and should ideally not be overwritten. ##### sample @@ -136,7 +136,7 @@ Please also run `self._validate_whitelist(pairs)` and to check and remove pairs # Generate dynamic whitelist pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key) # Validate whitelist to only have active market pairs - self._whitelist = self._validate_whitelist(pairs)[:self._number_pairs] + self._whitelist = self.validate_whitelist(pairs)[:self._number_pairs] ``` #### _gen_pair_whitelist From bba8e614094605c254bed875f0a846026305a437 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 19:30:35 +0100 Subject: [PATCH 100/319] Rename function in samples --- docs/strategy_analysis_example.md | 6 +++--- user_data/notebooks/strategy_analysis_example.ipynb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 55f1bd908..aa4578ca7 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -110,14 +110,14 @@ trades.groupby("pair")["sell_reason"].value_counts() ## Analyze the loaded trades for trade parallelism This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`. -`parallel_trade_analysis()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle. +`analyze_trade_parallelism()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle. ```python -from freqtrade.data.btanalysis import parallel_trade_analysis +from freqtrade.data.btanalysis import analyze_trade_parallelism # Analyze the above -parallel_trades = parallel_trade_analysis(trades, '5m') +parallel_trades = analyze_trade_parallelism(trades, '5m') parallel_trades.plot() diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/user_data/notebooks/strategy_analysis_example.ipynb index edb05a7ca..03dc83b4e 100644 --- a/user_data/notebooks/strategy_analysis_example.ipynb +++ b/user_data/notebooks/strategy_analysis_example.ipynb @@ -174,7 +174,7 @@ "## Analyze the loaded trades for trade parallelism\n", "This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.\n", "\n", - "`parallel_trade_analysis()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle." + "`analyze_trade_parallelism()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle." ] }, { @@ -183,10 +183,10 @@ "metadata": {}, "outputs": [], "source": [ - "from freqtrade.data.btanalysis import parallel_trade_analysis\n", + "from freqtrade.data.btanalysis import analyze_trade_parallelism\n", "\n", "# Analyze the above\n", - "parallel_trades = parallel_trade_analysis(trades, '5m')\n", + "parallel_trades = analyze_trade_parallelism(trades, '5m')\n", "\n", "\n", "parallel_trades.plot()" From 9e988783de6218a441f3a1bbd52dea59a9b61d6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 20:06:49 +0100 Subject: [PATCH 101/319] Allow configuration of stoploss on exchange limit fixes #1717 --- docs/configuration.md | 8 +++++++- freqtrade/freqtradebot.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1ad13c87a..217e9f37b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -215,6 +215,11 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and `emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails. The below is the default which is used if this is not configured in either strategy or configuration file. +Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. +`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1%. +Calculation example: we bought the asset at 100$. +Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$. + Syntax for Strategy: ```python @@ -224,7 +229,8 @@ order_types = { "emergencysell": "market", "stoploss": "market", "stoploss_on_exchange": False, - "stoploss_on_exchange_interval": 60 + "stoploss_on_exchange_interval": 60, + "stoploss_on_exchange_limit_ratio": 0.99, } ``` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a8fc6bc7e..4a494696d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -634,8 +634,8 @@ class FreqtradeBot: Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. :return: True if the order succeeded, and False in case of problems. """ - # Limit price threshold: As limit price should always be below price - LIMIT_PRICE_PCT = 0.99 + # Limit price threshold: As limit price should always be below stop-price + LIMIT_PRICE_PCT = self.strategy.order_types.get('stoploss_on_exchange_limit_ratio', 0.99) try: stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount, From 365a408df53b2a646bfe1f622544dac63c376119 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 06:43:42 +0100 Subject: [PATCH 102/319] Update release-documentation to fit new release style --- docs/developer.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 391493b09..67c0912a7 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -204,14 +204,15 @@ This part of the documentation is aimed at maintainers, and shows how to create ### Create release branch -``` bash -# make sure you're in develop branch -git checkout develop +First, pick a commit that's about one week old (to not include latest additions to releases). +``` bash # create new branch -git checkout -b new_release +git checkout -b new_release ``` +Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these. + * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * Commit this part * push that branch to the remote and create a PR against the master branch @@ -219,23 +220,18 @@ git checkout -b new_release ### Create changelog from git commits !!! Note - Make sure that both master and develop are up-todate!. + Make sure that the master branch is uptodate! ``` bash # Needs to be done before merging / pulling that branch. -git log --oneline --no-decorate --no-merges master..develop +git log --oneline --no-decorate --no-merges master..new_release ``` ### 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 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). -* Use the above changelog as release comment (as codeblock) - -### After-release - -* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`). -* Create a PR against develop to update that branch. +* Use the above changelog as release comment (as codeblock). From 5dcf28cafb0e29d06a4830a5c31cecdec377f110 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 06:51:36 +0100 Subject: [PATCH 103/319] Reduce frequency of "startup-period" message --- freqtrade/data/history.py | 3 ++- tests/data/test_history.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 412b086c0..b1e4313ca 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -148,7 +148,6 @@ def load_pair_history(pair: str, timerange_startup = deepcopy(timerange) if startup_candles > 0 and timerange_startup: - logger.info('Using indicator startup period: %s ...', startup_candles) timerange_startup.subtract_start(timeframe_to_seconds(ticker_interval) * startup_candles) # The user forced the refresh of pairs @@ -204,6 +203,8 @@ def load_data(datadir: Path, exchange and refresh_pairs are then not needed here nor in load_pair_history. """ result: Dict[str, DataFrame] = {} + if startup_candles > 0 and timerange: + logger.info(f'Using indicator startup period: {startup_candles} ...') for pair in pairs: hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, diff --git a/tests/data/test_history.py b/tests/data/test_history.py index d9627a0e4..89120b4f5 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -103,9 +103,7 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> datadir=testdatadir, timerange=timerange, startup_candles=20, ) - assert log_has( - 'Using indicator startup period: 20 ...', caplog - ) + assert ltfmock.call_count == 1 assert ltfmock.call_args_list[0][1]['timerange'] != timerange # startts is 20 minutes earlier @@ -354,8 +352,12 @@ def test_load_partial_missing(testdatadir, caplog) -> None: start = arrow.get('2018-01-01T00:00:00') end = arrow.get('2018-01-11T00:00:00') tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'], + startup_candles=20, timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) + assert log_has( + 'Using indicator startup period: 20 ...', caplog + ) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(tickerdata['UNITTEST/BTC']) From dc5f1b28785c366de20ffe07cb1296067e1f5405 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 07:08:02 +0100 Subject: [PATCH 104/319] Extract integration tests into sepearte file --- tests/test_freqtradebot.py | 108 ---------------------------------- tests/test_integration.py | 117 +++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 108 deletions(-) create mode 100644 tests/test_integration.py diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 30f9ba0a4..8538c7f41 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2526,114 +2526,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 -def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, - ticker, fee, - limit_buy_order, - markets, mocker) -> None: - """ - Tests workflow of selling stoploss_on_exchange. - Sells - * first trade as stoploss - * 2nd trade is kept - * 3rd trade is sold via sell-signal - """ - default_conf['max_open_trades'] = 3 - default_conf['exchange']['name'] = 'binance' - patch_RPCManager(mocker) - patch_exchange(mocker) - - stoploss_limit = { - 'id': 123, - 'info': {} - } - stoploss_order_open = { - "id": "123", - "timestamp": 1542707426845, - "datetime": "2018-11-20T09:50:26.845Z", - "lastTradeTimestamp": None, - "symbol": "BTC/USDT", - "type": "stop_loss_limit", - "side": "sell", - "price": 1.08801, - "amount": 90.99181074, - "cost": 0.0, - "average": 0.0, - "filled": 0.0, - "remaining": 0.0, - "status": "open", - "fee": None, - "trades": None - } - stoploss_order_closed = stoploss_order_open.copy() - stoploss_order_closed['status'] = 'closed' - # Sell first trade based on stoploss, keep 2nd and 3rd trade open - stoploss_order_mock = MagicMock( - side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) - # Sell 3rd trade (not called for the first trade) - should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)] - ) - cancel_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets), - symbol_amount_prec=lambda s, x, y: y, - symbol_price_prec=lambda s, x, y: y, - get_order=stoploss_order_mock, - cancel_order=cancel_order_mock, - ) - - wallets_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - create_stoploss_order=MagicMock(return_value=True), - update_trade_state=MagicMock(), - _notify_sell=MagicMock(), - ) - mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) - mocker.patch("freqtrade.wallets.Wallets.update", wallets_mock) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - # Switch ordertype to market to close trade immediately - freqtrade.strategy.order_types['sell'] = 'market' - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trades() - wallets_mock.reset_mock() - Trade.session = MagicMock() - - trades = Trade.query.all() - # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) - for trade in trades: - trade.stoploss_order_id = 3 - trade.open_order_id = None - - freqtrade.process_maybe_execute_sells(trades) - assert should_sell_mock.call_count == 2 - - # Only order for 3rd trade needs to be cancelled - assert cancel_order_mock.call_count == 1 - # Wallets should only be called once per sell cycle - assert wallets_mock.call_count == 1 - - trade = trades[0] - assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value - assert not trade.is_open - - trade = trades[1] - assert not trade.sell_reason - assert trade.is_open - - trade = trades[2] - assert trade.sell_reason == SellType.SELL_SIGNAL.value - assert not trade.is_open - def test_execute_sell_market_order(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 000000000..857e0a2e3 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,117 @@ + +from unittest.mock import MagicMock, PropertyMock + + +from freqtrade.freqtradebot import FreqtradeBot +from freqtrade.persistence import Trade +from freqtrade.strategy.interface import SellCheckTuple, SellType +from tests.conftest import (patch_exchange, + patch_get_signal) + + +def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, + ticker, fee, + limit_buy_order, + markets, mocker) -> None: + """ + Tests workflow of selling stoploss_on_exchange. + Sells + * first trade as stoploss + * 2nd trade is kept + * 3rd trade is sold via sell-signal + """ + default_conf['max_open_trades'] = 3 + default_conf['exchange']['name'] = 'binance' + patch_exchange(mocker) + + stoploss_limit = { + 'id': 123, + 'info': {} + } + stoploss_order_open = { + "id": "123", + "timestamp": 1542707426845, + "datetime": "2018-11-20T09:50:26.845Z", + "lastTradeTimestamp": None, + "symbol": "BTC/USDT", + "type": "stop_loss_limit", + "side": "sell", + "price": 1.08801, + "amount": 90.99181074, + "cost": 0.0, + "average": 0.0, + "filled": 0.0, + "remaining": 0.0, + "status": "open", + "fee": None, + "trades": None + } + stoploss_order_closed = stoploss_order_open.copy() + stoploss_order_closed['status'] = 'closed' + # Sell first trade based on stoploss, keep 2nd and 3rd trade open + stoploss_order_mock = MagicMock( + side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) + # Sell 3rd trade (not called for the first trade) + should_sell_mock = MagicMock(side_effect=[ + SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), + SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)] + ) + cancel_order_mock = MagicMock() + mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets), + symbol_amount_prec=lambda s, x, y: y, + symbol_price_prec=lambda s, x, y: y, + get_order=stoploss_order_mock, + cancel_order=cancel_order_mock, + ) + + wallets_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.freqtradebot.FreqtradeBot', + create_stoploss_order=MagicMock(return_value=True), + update_trade_state=MagicMock(), + _notify_sell=MagicMock(), + ) + mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) + mocker.patch("freqtrade.wallets.Wallets.update", wallets_mock) + + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + # Switch ordertype to market to close trade immediately + freqtrade.strategy.order_types['sell'] = 'market' + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trades() + wallets_mock.reset_mock() + Trade.session = MagicMock() + + trades = Trade.query.all() + # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) + for trade in trades: + trade.stoploss_order_id = 3 + trade.open_order_id = None + + freqtrade.process_maybe_execute_sells(trades) + assert should_sell_mock.call_count == 2 + + # Only order for 3rd trade needs to be cancelled + assert cancel_order_mock.call_count == 1 + # Wallets should only be called once per sell cycle + assert wallets_mock.call_count == 1 + + trade = trades[0] + assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value + assert not trade.is_open + + trade = trades[1] + assert not trade.sell_reason + assert trade.is_open + + trade = trades[2] + assert trade.sell_reason == SellType.SELL_SIGNAL.value + assert not trade.is_open From ce6b869f84d8d0dc0ef5d0b5c28b4bec792c36d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 07:11:57 +0100 Subject: [PATCH 105/319] Cleanup test --- tests/test_integration.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 857e0a2e3..c3a3ada07 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -2,10 +2,9 @@ from unittest.mock import MagicMock, PropertyMock -from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.strategy.interface import SellCheckTuple, SellType -from tests.conftest import (patch_exchange, +from tests.conftest import (patch_exchange, get_patched_freqtradebot, patch_get_signal) @@ -22,7 +21,6 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, """ default_conf['max_open_trades'] = 3 default_conf['exchange']['name'] = 'binance' - patch_exchange(mocker) stoploss_limit = { 'id': 123, @@ -62,14 +60,12 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, get_order=stoploss_order_mock, cancel_order=cancel_order_mock, ) - wallets_mock = MagicMock() mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', create_stoploss_order=MagicMock(return_value=True), @@ -77,9 +73,9 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, _notify_sell=MagicMock(), ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) - mocker.patch("freqtrade.wallets.Wallets.update", wallets_mock) + wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) - freqtrade = FreqtradeBot(default_conf) + freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True # Switch ordertype to market to close trade immediately freqtrade.strategy.order_types['sell'] = 'market' From 734a9d5d87717bf4ed1be8c6d8b718aeaa2b81da Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 07:15:45 +0100 Subject: [PATCH 106/319] Seperate tests related to worker from test_freqtradebot --- tests/test_freqtradebot.py | 75 ----------------------------------- tests/test_integration.py | 6 +-- tests/test_worker.py | 81 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 79 deletions(-) create mode 100644 tests/test_worker.py diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8538c7f41..1f761b55f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -14,7 +14,6 @@ import requests from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError, constants) from freqtrade.constants import MATH_CLOSE_PREC -from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType @@ -49,16 +48,6 @@ def test_freqtradebot_state(mocker, default_conf, markets) -> None: assert freqtrade.state is State.STOPPED -def test_worker_state(mocker, default_conf, markets) -> None: - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - worker = get_patched_worker(mocker, default_conf) - assert worker.state is State.RUNNING - - default_conf.pop('initial_state') - worker = Worker(args=None, config=default_conf) - assert worker.state is State.STOPPED - - def test_cleanup(mocker, default_conf, caplog) -> None: mock_cleanup = MagicMock() mocker.patch('freqtrade.persistence.cleanup', mock_cleanup) @@ -68,69 +57,6 @@ def test_cleanup(mocker, default_conf, caplog) -> None: assert mock_cleanup.call_count == 1 -def test_worker_running(mocker, default_conf, caplog) -> None: - mock_throttle = MagicMock() - mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) - mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock()) - - worker = get_patched_worker(mocker, default_conf) - - state = worker._worker(old_state=None) - assert state is State.RUNNING - 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 - assert worker.freqtrade.strategy.dp - assert isinstance(worker.freqtrade.strategy.dp, DataProvider) - - -def test_worker_stopped(mocker, default_conf, caplog) -> None: - mock_throttle = MagicMock() - mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) - mock_sleep = mocker.patch('time.sleep', return_value=None) - - worker = get_patched_worker(mocker, default_conf) - worker.state = State.STOPPED - state = worker._worker(old_state=State.RUNNING) - assert state is State.STOPPED - assert log_has('Changing state to: STOPPED', caplog) - assert mock_throttle.call_count == 0 - assert mock_sleep.call_count == 1 - - -def test_throttle(mocker, default_conf, caplog) -> None: - def throttled_func(): - return 42 - - caplog.set_level(logging.DEBUG) - worker = get_patched_worker(mocker, default_conf) - - start = time.time() - result = worker._throttle(throttled_func, min_secs=0.1) - end = time.time() - - assert result == 42 - assert end - start > 0.1 - assert log_has('Throttling throttled_func for 0.10 seconds', caplog) - - result = worker._throttle(throttled_func, min_secs=-1) - assert result == 42 - - -def test_throttle_with_assets(mocker, default_conf) -> None: - def throttled_func(nb_assets=-1): - return nb_assets - - worker = get_patched_worker(mocker, default_conf) - - result = worker._throttle(throttled_func, min_secs=0.1, nb_assets=666) - assert result == 666 - - result = worker._throttle(throttled_func, min_secs=0.1) - assert result == -1 - - def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -2526,7 +2452,6 @@ 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) diff --git a/tests/test_integration.py b/tests/test_integration.py index c3a3ada07..37ee24d6c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,11 +1,9 @@ -from unittest.mock import MagicMock, PropertyMock - +from unittest.mock import MagicMock from freqtrade.persistence import Trade from freqtrade.strategy.interface import SellCheckTuple, SellType -from tests.conftest import (patch_exchange, get_patched_freqtradebot, - patch_get_signal) +from tests.conftest import get_patched_freqtradebot, patch_get_signal def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, diff --git a/tests/test_worker.py b/tests/test_worker.py new file mode 100644 index 000000000..72e215210 --- /dev/null +++ b/tests/test_worker.py @@ -0,0 +1,81 @@ +import logging +import time +from unittest.mock import MagicMock, PropertyMock + +from freqtrade.data.dataprovider import DataProvider +from freqtrade.state import State +from freqtrade.worker import Worker +from tests.conftest import get_patched_worker, log_has + + +def test_worker_state(mocker, default_conf, markets) -> None: + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + worker = get_patched_worker(mocker, default_conf) + assert worker.state is State.RUNNING + + default_conf.pop('initial_state') + worker = Worker(args=None, config=default_conf) + assert worker.state is State.STOPPED + + +def test_worker_running(mocker, default_conf, caplog) -> None: + mock_throttle = MagicMock() + mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) + mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock()) + + worker = get_patched_worker(mocker, default_conf) + + state = worker._worker(old_state=None) + assert state is State.RUNNING + 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 + assert worker.freqtrade.strategy.dp + assert isinstance(worker.freqtrade.strategy.dp, DataProvider) + + +def test_worker_stopped(mocker, default_conf, caplog) -> None: + mock_throttle = MagicMock() + mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) + mock_sleep = mocker.patch('time.sleep', return_value=None) + + worker = get_patched_worker(mocker, default_conf) + worker.state = State.STOPPED + state = worker._worker(old_state=State.RUNNING) + assert state is State.STOPPED + assert log_has('Changing state to: STOPPED', caplog) + assert mock_throttle.call_count == 0 + assert mock_sleep.call_count == 1 + + +def test_throttle(mocker, default_conf, caplog) -> None: + def throttled_func(): + return 42 + + caplog.set_level(logging.DEBUG) + worker = get_patched_worker(mocker, default_conf) + + start = time.time() + result = worker._throttle(throttled_func, min_secs=0.1) + end = time.time() + + assert result == 42 + assert end - start > 0.1 + assert log_has('Throttling throttled_func for 0.10 seconds', caplog) + + result = worker._throttle(throttled_func, min_secs=-1) + assert result == 42 + + +def test_throttle_with_assets(mocker, default_conf) -> None: + def throttled_func(nb_assets=-1): + return nb_assets + + worker = get_patched_worker(mocker, default_conf) + + result = worker._throttle(throttled_func, min_secs=0.1, nb_assets=666) + assert result == 666 + + result = worker._throttle(throttled_func, min_secs=0.1) + assert result == -1 From 7be378aaa919befbdbc6fa27dd942638f4a306bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 07:26:48 +0100 Subject: [PATCH 107/319] Remove markets mock where it's not needed --- tests/test_freqtradebot.py | 157 ++++++++++++------------------------- tests/test_integration.py | 6 +- 2 files changed, 51 insertions(+), 112 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1f761b55f..0ed8e8a77 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -150,18 +150,13 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade._get_trade_stake_amount('ETH/BTC') -def test_get_trade_stake_amount_unlimited_amount(default_conf, - ticker, - limit_buy_order, - fee, - markets, - mocker) -> None: +def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, + limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount']) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee @@ -222,7 +217,7 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: assert freqtrade._get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 -def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, edge_conf) -> None: +def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -243,7 +238,6 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) ############################################# @@ -263,7 +257,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, assert trade.sell_reason == SellType.STOP_LOSS.value -def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, +def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -284,7 +278,6 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) ############################################# @@ -303,7 +296,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, def test_total_open_trades_stakes(mocker, default_conf, ticker, - limit_buy_order, fee, markets) -> None: + limit_buy_order, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['stake_amount'] = 0.0000098751 @@ -313,7 +306,6 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -448,7 +440,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: assert result == min(8, 2 * 2) / 0.9 -def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: +def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -456,7 +448,6 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mock get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) # Save state of current whitelist @@ -482,7 +473,7 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mock def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) @@ -491,7 +482,6 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -501,7 +491,7 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -510,7 +500,6 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, get_ticker=ticker, buy=buy_mock, get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['stake_amount'] = 0.0005 freqtrade = FreqtradeBot(default_conf) @@ -522,7 +511,7 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -531,7 +520,6 @@ def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_or get_ticker=ticker, buy=buy_mock, get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['stake_amount'] = 0.000000005 @@ -551,7 +539,6 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['max_open_trades'] = 0 default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT @@ -564,7 +551,7 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, - markets, mocker, caplog) -> None: + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -572,7 +559,6 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] @@ -586,7 +572,7 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - markets, mocker, caplog) -> None: + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -594,7 +580,6 @@ def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_ord get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['exchange']['pair_whitelist'] = [] freqtrade = FreqtradeBot(default_conf) @@ -625,7 +610,7 @@ def test_create_trades_no_signal(default_conf, fee, mocker) -> None: @pytest.mark.parametrize("max_open", range(0, 5)) def test_create_trades_multiple_trades(default_conf, ticker, - fee, markets, mocker, max_open) -> None: + fee, mocker, max_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['max_open_trades'] = max_open @@ -634,7 +619,6 @@ def test_create_trades_multiple_trades(default_conf, ticker, 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) @@ -645,7 +629,7 @@ 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: +def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['max_open_trades'] = 4 @@ -654,7 +638,6 @@ def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> No 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) @@ -673,13 +656,12 @@ def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> No def test_process_trade_creation(default_conf, ticker, limit_buy_order, - markets, fee, mocker, caplog) -> None: + fee, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -708,13 +690,12 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, ) -def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> None: +def test_process_exchange_failures(default_conf, ticker, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(side_effect=TemporaryError) ) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) @@ -726,13 +707,12 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non assert sleep_mock.has_calls() -def test_process_operational_exception(default_conf, ticker, markets, mocker) -> None: +def test_process_operational_exception(default_conf, ticker, mocker) -> None: msg_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(side_effect=OperationalException) ) worker = Worker(args=None, config=default_conf) @@ -745,14 +725,12 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] -def test_process_trade_handling( - default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None: +def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -772,15 +750,14 @@ def test_process_trade_handling( assert len(trades) == 1 -def test_process_trade_no_whitelist_pair( - default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None: +def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, + fee, mocker) -> None: """ Test process with trade not in pair list """ patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -817,7 +794,7 @@ def test_process_trade_no_whitelist_pair( assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist)) -def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) -> None: +def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -828,7 +805,6 @@ def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(side_effect=TemporaryError), refresh_latest_ohlcv=refresh_mock, ) @@ -874,7 +850,7 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None: assert freqtrade.get_target_bid('ETH/BTC') == 5 -def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None: +def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf) @@ -896,7 +872,6 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non }), buy=buy_mm, get_fee=fee, - markets=PropertyMock(return_value=markets) ) pair = 'ETH/BTC' @@ -993,7 +968,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, - markets, limit_buy_order, limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: stoploss_limit = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) patch_exchange(mocker) @@ -1007,7 +982,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 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), stoploss_limit=stoploss_limit ) freqtrade = FreqtradeBot(default_conf) @@ -1094,7 +1068,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, - markets, limit_buy_order, limit_sell_order) -> None: + 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) @@ -1108,7 +1082,6 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, 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()), ) @@ -1129,7 +1102,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, - markets, limit_buy_order, limit_sell_order): + limit_buy_order, limit_sell_order): rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) @@ -1143,7 +1116,6 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=sell_mock, get_fee=fee, - markets=PropertyMock(return_value=markets), get_order=MagicMock(return_value={'status': 'canceled'}), stoploss_limit=MagicMock(side_effect=InvalidOrderException()), ) @@ -1266,8 +1238,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, - markets, limit_buy_order, - limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set stoploss_limit = MagicMock(return_value={'id': 13434334}) patch_exchange(mocker) @@ -1282,7 +1253,6 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 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), stoploss_limit=stoploss_limit ) @@ -1335,7 +1305,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, - markets, limit_buy_order, limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set stoploss_limit = MagicMock(return_value={'id': 13434334}) @@ -1353,7 +1323,6 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, 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), stoploss_limit=stoploss_limit ) @@ -1654,8 +1623,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde assert not trade.is_open -def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, - fee, markets, mocker) -> None: +def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1668,7 +1636,6 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, 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) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1695,8 +1662,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, assert trade.close_date is not None -def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1704,7 +1670,6 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) @@ -1810,7 +1775,7 @@ def test_handle_trade_use_sell_signal( def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1818,7 +1783,6 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2148,14 +2112,13 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None: assert cancel_order_mock.call_count == 1 -def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: +def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) @@ -2195,14 +2158,13 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc } == last_msg -def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: +def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) @@ -2244,15 +2206,13 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee, - ticker_sell_down, - markets, mocker) -> None: + ticker_sell_down, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) @@ -2300,8 +2260,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe } == last_msg -def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, - markets, caplog) -> None: +def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) sellmock = MagicMock() @@ -2310,7 +2269,6 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), sell=sellmock ) @@ -2330,9 +2288,8 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, assert log_has('Could not cancel stoploss order abcd', caplog) -def test_execute_sell_with_stoploss_on_exchange(default_conf, - ticker, fee, ticker_sell_up, - markets, mocker) -> None: +def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, + mocker) -> None: default_conf['exchange']['name'] = 'binance' rpc_mock = patch_RPCManager(mocker) @@ -2349,7 +2306,6 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, stoploss_limit=stoploss_limit, @@ -2384,10 +2340,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, assert rpc_mock.call_count == 2 -def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, - ticker, fee, - limit_buy_order, - markets, mocker) -> None: +def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, + limit_buy_order, mocker) -> None: default_conf['exchange']['name'] = 'binance' rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) @@ -2395,7 +2349,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, ) @@ -2453,14 +2406,13 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, def test_execute_sell_market_order(default_conf, ticker, fee, - ticker_sell_up, markets, mocker) -> None: + ticker_sell_up, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) @@ -2506,7 +2458,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2518,7 +2470,6 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'use_sell_signal': True, @@ -2538,7 +2489,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2550,7 +2501,6 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'use_sell_signal': True, @@ -2568,7 +2518,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: +def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2580,7 +2530,6 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'use_sell_signal': True, @@ -2598,7 +2547,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market assert freqtrade.handle_trade(trade) is False -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: +def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2610,7 +2559,6 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'use_sell_signal': True, @@ -2932,7 +2880,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2944,7 +2892,6 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'ignore_roi_if_buy_signal': False @@ -3239,7 +3186,7 @@ def test_get_real_amount_open_trade(default_conf, mocker): assert freqtrade.get_real_amount(trade, order) == amount -def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, markets, mocker, +def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1 @@ -3251,7 +3198,6 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) # Save state of current whitelist @@ -3275,7 +3221,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order, - fee, markets, mocker, order_book_l2): + fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True # delta is 100 which is impossible to reach. hence check_depth_of_market will return false default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 100 @@ -3287,7 +3233,6 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) # Save state of current whitelist freqtrade = FreqtradeBot(default_conf) @@ -3298,7 +3243,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o assert trade is None -def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) -> None: +def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: """ test if function get_target_bid will return the order book price instead of the ask rate @@ -3307,7 +3252,6 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_order_book=order_book_l2, get_ticker=ticker_mock, @@ -3323,7 +3267,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) assert ticker_mock.call_count == 0 -def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None: +def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None: """ test if function get_target_bid will return the ask rate (since its value is lower) instead of the order book rate (even if enabled) @@ -3332,7 +3276,6 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_order_book=order_book_l2, get_ticker=ticker_mock, @@ -3349,14 +3292,13 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) assert ticker_mock.call_count == 0 -def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None: +def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: """ test check depth of market """ patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_order_book=order_book_l2 ) default_conf['telegram']['enabled'] = False @@ -3371,7 +3313,7 @@ def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order, - fee, markets, mocker, order_book_l2) -> None: + fee, mocker, order_book_l2) -> None: """ test order book ask strategy """ @@ -3393,7 +3335,6 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order 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) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) diff --git a/tests/test_integration.py b/tests/test_integration.py index 37ee24d6c..c83b2de6d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,10 +6,8 @@ from freqtrade.strategy.interface import SellCheckTuple, SellType from tests.conftest import get_patched_freqtradebot, patch_get_signal -def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, - ticker, fee, - limit_buy_order, - markets, mocker) -> None: +def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, + limit_buy_order, mocker) -> None: """ Tests workflow of selling stoploss_on_exchange. Sells From b6616d7a13e727adf6a43f61d321fdaff88e4cfa Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 10:04:28 +0100 Subject: [PATCH 108/319] Add test helping debug #1985 --- tests/test_integration.py | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_integration.py b/tests/test_integration.py index c83b2de6d..228ed8468 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from freqtrade.persistence import Trade from freqtrade.strategy.interface import SellCheckTuple, SellType from tests.conftest import get_patched_freqtradebot, patch_get_signal +from freqtrade.rpc.rpc import RPC def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, @@ -107,3 +108,52 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, trade = trades[2] assert trade.sell_reason == SellType.SELL_SIGNAL.value assert not trade.is_open + + +def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker) -> None: + """ + Tests workflow + """ + default_conf['max_open_trades'] = 5 + default_conf['forcebuy_enable'] = True + default_conf['stake_amount'] = 'unlimited' + default_conf['exchange']['name'] = 'binance' + default_conf['telegram']['enabled'] = True + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock( + side_effect=[1000, 800, 600, 400, 200] + )) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + get_fee=fee, + symbol_amount_prec=lambda s, x, y: y, + symbol_price_prec=lambda s, x, y: y, + ) + + mocker.patch.multiple( + 'freqtrade.freqtradebot.FreqtradeBot', + create_stoploss_order=MagicMock(return_value=True), + update_trade_state=MagicMock(), + _notify_sell=MagicMock(), + ) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(freqtrade) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + # Switch ordertype to market to close trade immediately + freqtrade.strategy.order_types['sell'] = 'market' + patch_get_signal(freqtrade) + + # Create 4 trades + freqtrade.create_trades() + + trades = Trade.query.all() + assert len(trades) == 4 + rpc._rpc_forcebuy('TKN/BTC', None) + + trades = Trade.query.all() + assert len(trades) == 5 + + for trade in trades: + assert trade.stake_amount == 200 From 9a42afe0bec5442b4f0805ba190211829efde22d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 10:39:24 +0100 Subject: [PATCH 109/319] Move exchange-constants and retriers to exchange.common --- freqtrade/exchange/__init__.py | 3 +- freqtrade/exchange/common.py | 124 ++++++++++++++++++++++++++++++++ freqtrade/exchange/exchange.py | 123 +------------------------------ tests/exchange/test_exchange.py | 8 +-- 4 files changed, 132 insertions(+), 126 deletions(-) create mode 100644 freqtrade/exchange/common.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 0948692f1..c107f7abc 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,4 +1,5 @@ -from freqtrade.exchange.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS # noqa: F401 +from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS # noqa: F401 +from freqtrade.exchange.exchange import Exchange # noqa: F401 from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401 is_exchange_bad, is_exchange_known_ccxt, diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py new file mode 100644 index 000000000..ed30b95c7 --- /dev/null +++ b/freqtrade/exchange/common.py @@ -0,0 +1,124 @@ +import logging + +from freqtrade import DependencyException, TemporaryError + +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", + "hitbtc": "This API cannot be used with Freqtrade. " + "Use `hitbtc2` exchange id to access this exchange.", + **dict.fromkeys([ + 'adara', + 'anxpro', + 'bigone', + 'coinbase', + 'coinexchange', + 'coinmarketcap', + 'lykke', + 'xbtce', + ], "Does not provide timeframes. ccxt fetchOHLCV: False"), + **dict.fromkeys([ + 'bcex', + 'bit2c', + 'bitbay', + 'bitflyer', + 'bitforex', + 'bithumb', + 'bitso', + 'bitstamp1', + 'bl3p', + 'braziliex', + 'btcbox', + 'btcchina', + 'btctradeim', + 'btctradeua', + 'bxinth', + 'chilebit', + 'coincheck', + 'coinegg', + 'coinfalcon', + 'coinfloor', + 'coingi', + 'coinmate', + 'coinone', + 'coinspot', + 'coolcoin', + 'crypton', + 'deribit', + 'exmo', + 'exx', + 'flowbtc', + 'foxbit', + 'fybse', + # 'hitbtc', + 'ice3x', + 'independentreserve', + 'indodax', + 'itbit', + 'lakebtc', + 'latoken', + 'liquid', + 'livecoin', + 'luno', + 'mixcoins', + 'negociecoins', + 'nova', + 'paymium', + 'southxchange', + 'stronghold', + 'surbitcoin', + 'therock', + 'tidex', + 'vaultoro', + 'vbtc', + 'virwox', + 'yobit', + 'zaif', + ], "Does not provide timeframes. ccxt fetchOHLCV: emulated"), +} + +MAP_EXCHANGE_CHILDCLASS = { + 'binanceus': 'binance', + 'binanceje': 'binance', +} + + +def retrier_async(f): + async def wrapper(*args, **kwargs): + count = kwargs.pop('count', API_RETRY_COUNT) + try: + return await f(*args, **kwargs) + except (TemporaryError, DependencyException) as ex: + logger.warning('%s() returned exception: "%s"', f.__name__, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warning('retrying %s() still for %s times', f.__name__, count) + return await wrapper(*args, **kwargs) + else: + logger.warning('Giving up retrying: %s()', f.__name__) + raise ex + return wrapper + + +def retrier(f): + def wrapper(*args, **kwargs): + count = kwargs.pop('count', API_RETRY_COUNT) + try: + return f(*args, **kwargs) + except (TemporaryError, DependencyException) as ex: + logger.warning('%s() returned exception: "%s"', f.__name__, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warning('retrying %s() still for %s times', f.__name__, count) + return wrapper(*args, **kwargs) + else: + logger.warning('Giving up retrying: %s()', f.__name__) + raise ex + return wrapper diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 023e16cc5..430a2ff54 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,137 +14,18 @@ from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt import ccxt.async_support as ccxt_async -from ccxt.base.decimal_to_precision import ROUND_UP, ROUND_DOWN +from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError, constants) from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts - 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", - "hitbtc": "This API cannot be used with Freqtrade. " - "Use `hitbtc2` exchange id to access this exchange.", - **dict.fromkeys([ - 'adara', - 'anxpro', - 'bigone', - 'coinbase', - 'coinexchange', - 'coinmarketcap', - 'lykke', - 'xbtce', - ], "Does not provide timeframes. ccxt fetchOHLCV: False"), - **dict.fromkeys([ - 'bcex', - 'bit2c', - 'bitbay', - 'bitflyer', - 'bitforex', - 'bithumb', - 'bitso', - 'bitstamp1', - 'bl3p', - 'braziliex', - 'btcbox', - 'btcchina', - 'btctradeim', - 'btctradeua', - 'bxinth', - 'chilebit', - 'coincheck', - 'coinegg', - 'coinfalcon', - 'coinfloor', - 'coingi', - 'coinmate', - 'coinone', - 'coinspot', - 'coolcoin', - 'crypton', - 'deribit', - 'exmo', - 'exx', - 'flowbtc', - 'foxbit', - 'fybse', - # 'hitbtc', - 'ice3x', - 'independentreserve', - 'indodax', - 'itbit', - 'lakebtc', - 'latoken', - 'liquid', - 'livecoin', - 'luno', - 'mixcoins', - 'negociecoins', - 'nova', - 'paymium', - 'southxchange', - 'stronghold', - 'surbitcoin', - 'therock', - 'tidex', - 'vaultoro', - 'vbtc', - 'virwox', - 'yobit', - 'zaif', - ], "Does not provide timeframes. ccxt fetchOHLCV: emulated"), - } - -MAP_EXCHANGE_CHILDCLASS = { - 'binanceus': 'binance', - 'binanceje': 'binance', -} - - -def retrier_async(f): - async def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) - try: - return await f(*args, **kwargs) - except (TemporaryError, DependencyException) as ex: - logger.warning('%s() returned exception: "%s"', f.__name__, ex) - if count > 0: - count -= 1 - kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) - return await wrapper(*args, **kwargs) - else: - logger.warning('Giving up retrying: %s()', f.__name__) - raise ex - return wrapper - - -def retrier(f): - def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) - try: - return f(*args, **kwargs) - except (TemporaryError, DependencyException) as ex: - logger.warning('%s() returned exception: "%s"', f.__name__, ex) - if count > 0: - count -= 1 - kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) - return wrapper(*args, **kwargs) - else: - logger.warning('Giving up retrying: %s()', f.__name__) - raise ex - return wrapper - - class Exchange: _config: Dict = {} diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4eb0df1a3..8a4121d80 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -14,13 +14,13 @@ 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.common import API_RETRY_COUNT +from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, + timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds, - symbol_is_pair, - market_is_active) + timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re From a80e49bd8174d7a335c1393f0880e5e36543e1c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 12:49:41 +0100 Subject: [PATCH 110/319] Change level of rpi header --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 50f24d3e8..9180beb40 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -95,7 +95,7 @@ sudo apt-get update sudo apt-get install build-essential git ``` -#### Raspberry Pi / Raspbian +### Raspberry Pi / Raspbian The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. From 241d94756466c1a65b966ba8d3e8db81d5812e2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 15:39:25 +0100 Subject: [PATCH 111/319] Add new runmodes --- freqtrade/configuration/check_exchange.py | 3 ++- freqtrade/configuration/config_validation.py | 3 ++- freqtrade/state.py | 4 +++- freqtrade/utils.py | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index 5e811fb81..5d963db47 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -21,7 +21,8 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: and thus is not known for the Freqtrade at all. """ - if config['runmode'] in [RunMode.PLOT] and not config.get('exchange', {}).get('name'): + if (config['runmode'] in [RunMode.PLOT, RunMode.UTIL_NO_EXCHANGE] + and not config.get('exchange', {}).get('name')): # Skip checking exchange in plot mode, since it requires no exchange return True logger.info("Checking exchange...") diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 93d93263f..8083264e2 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -118,7 +118,8 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None: """ Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does. """ - if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT]: + if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT, + RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]: return if (conf.get('pairlist', {}).get('method', 'StaticPairList') == 'StaticPairList' diff --git a/freqtrade/state.py b/freqtrade/state.py index d4a2adba0..ed8c75def 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -25,5 +25,7 @@ class RunMode(Enum): BACKTEST = "backtest" EDGE = "edge" HYPEROPT = "hyperopt" + UTIL_EXCHANGE = "util_exchange" + UTIL_NO_EXCHANGE = "util_no_exchange" PLOT = "plot" - OTHER = "other" # Used for plotting scripts and test + OTHER = "other" diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 25e883c76..630de0f5a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -74,7 +74,7 @@ def start_download_data(args: Dict[str, Any]) -> None: """ Download data (former download_backtest_data.py script) """ - config = setup_utils_configuration(args, RunMode.OTHER) + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) timerange = TimeRange() if 'days' in config: @@ -123,7 +123,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: """ Print ticker intervals (timeframes) available on Exchange """ - config = setup_utils_configuration(args, RunMode.OTHER) + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Do not use ticker_interval set in the config config['ticker_interval'] = None @@ -144,7 +144,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: :param pairs_only: if True print only pairs, otherwise print all instruments (markets) :return: None """ - config = setup_utils_configuration(args, RunMode.OTHER) + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Init exchange exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange From 691cec7956e17256af27506216954b2cd6f043cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 16:42:57 +0100 Subject: [PATCH 112/319] Be more selective which startup-messages are shown --- freqtrade/configuration/configuration.py | 46 +++++++++++++----------- freqtrade/state.py | 5 +++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index be1c7ab4e..4637e3e5d 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -17,7 +17,7 @@ from freqtrade.configuration.directory_operations import (create_datadir, from freqtrade.configuration.load_config import load_config_file from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts, json_load -from freqtrade.state import RunMode +from freqtrade.state import RunMode, TRADING_MODES, NON_UTIL_MODES logger = logging.getLogger(__name__) @@ -98,14 +98,16 @@ class Configuration: # Keep a copy of the original configuration file config['original_config'] = deepcopy(config) + self._process_runmode(config) + self._process_common_options(config) + self._process_trading_options(config) + self._process_optimize_options(config) self._process_plot_options(config) - 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)) @@ -130,6 +132,22 @@ class Configuration: setup_logging(config) + def _process_trading_options(self, config: Dict[str, Any]) -> None: + if config['runmode'] not in TRADING_MODES: + return + + if config.get('dry_run', False): + logger.info('Dry run is enabled') + if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: + # Default to in-memory db for dry_run if not specified + config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL + else: + if not config.get('db_url', None): + config['db_url'] = constants.DEFAULT_DB_PROD_URL + logger.info('Dry run is disabled') + + logger.info(f'Using DB: "{config["db_url"]}"') + def _process_common_options(self, config: Dict[str, Any]) -> None: self._process_logging_options(config) @@ -146,25 +164,9 @@ class Configuration: config.update({'db_url': self.args["db_url"]}) logger.info('Parameter --db-url detected ...') - if config.get('dry_run', False): - logger.info('Dry run is enabled') - if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: - # Default to in-memory db for dry_run if not specified - config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL - else: - if not config.get('db_url', None): - config['db_url'] = constants.DEFAULT_DB_PROD_URL - logger.info('Dry run is disabled') - - logger.info(f'Using DB: "{config["db_url"]}"') - if config.get('forcebuy_enable', False): logger.warning('`forcebuy` RPC message enabled.') - # Setting max_open_trades to infinite if -1 - if config.get('max_open_trades') == -1: - config['max_open_trades'] = float('inf') - # Support for sd_notify if 'sd_notify' in self.args and self.args["sd_notify"]: config['internals'].update({'sd_notify': True}) @@ -212,6 +214,10 @@ class Configuration: self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') + # Setting max_open_trades to infinite if -1 + if config.get('max_open_trades') == -1: + config['max_open_trades'] = float('inf') + if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: config.update({'use_max_market_positions': False}) logger.info('Parameter --disable-max-market-positions detected ...') @@ -220,7 +226,7 @@ class Configuration: config.update({'max_open_trades': self.args["max_open_trades"]}) logger.info('Parameter --max_open_trades detected, ' 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) - else: + elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) self._args_to_config(config, argname='stake_amount', diff --git a/freqtrade/state.py b/freqtrade/state.py index ed8c75def..415f6f5f2 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -29,3 +29,8 @@ class RunMode(Enum): UTIL_NO_EXCHANGE = "util_no_exchange" PLOT = "plot" OTHER = "other" + + +TRADING_MODES = [RunMode.LIVE, RunMode.DRY_RUN] +OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT] +NON_UTIL_MODES = TRADING_MODES + OPTIMIZE_MODES From 2124661cee43affc660cb08f07af26d68004c35d Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 2 Nov 2019 02:22:58 +0300 Subject: [PATCH 113/319] Update faq with examples of grepping the log --- docs/faq.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index dd92d310e..7652ec278 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -55,6 +55,44 @@ If you have restricted pairs in your whitelist, you'll get a warning message in 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. +### How do I search the bot logs for something? + +By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility subcommands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. + +* In unix shells, this normally can be done as simple as: +```shell +freqtrade --some-options 2>&1 >/dev/null | grep 'something' +``` +(note, `2>&1` and `>/dev/null` should be written in this order) + +* Bash interpreter also supports so called process substitution syntax, you can grep the log for a string with it as: +```shell +$ freqtrade --some-options 2> >(grep 'something') >/dev/null +``` +or +```shell +$ freqtrade --some-options 2> >(grep -v 'something' 1>&2) +``` + +* You can also write the copy of Freqtrade log messages to a file with the `--logfile` option: +```shell +$ freqtrade --logfile /path/to/mylogfile.log --some-options +``` +and then grep it as: +```shell +$ cat /path/to/mylogfile.log | grep 'something' +``` +or even on the fly, as the bot works and the logfile grows: +```shell +$ tail -f /path/to/mylogfile.log | grep 'something' +``` +from a separate terminal window. + +On Windows, the `--logfilename` option is also supported by Freqtrade and you can use the `findstr` command to search the log for the string of interest: +``` +> type \path\to\mylogfile.log | findstr "something" +``` + ## Hyperopt module ### How many epoch do I need to get a good Hyperopt result? From e9af6b393f5ff7aa34e2e0d63879ced22741343e Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 2 Nov 2019 02:32:57 +0300 Subject: [PATCH 114/319] Fix typo --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 7652ec278..7fdd54958 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -61,7 +61,7 @@ By default, the bot writes its log into stderr stream. This is implemented this * In unix shells, this normally can be done as simple as: ```shell -freqtrade --some-options 2>&1 >/dev/null | grep 'something' +$ freqtrade --some-options 2>&1 >/dev/null | grep 'something' ``` (note, `2>&1` and `>/dev/null` should be written in this order) From 861f10dca65b64a76a3fd83459161b95e2a737ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 11:10:33 +0100 Subject: [PATCH 115/319] Allow populate-indicators to come from strategy --- freqtrade/optimize/hyperopt_interface.py | 12 +----------- freqtrade/resolvers/hyperopt_resolver.py | 3 +++ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 4208b29d3..142f305df 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,10 +5,9 @@ This module defines the interface to apply for hyperopts import logging import math -from abc import ABC, abstractmethod +from abc import ABC from typing import Dict, Any, Callable, List -from pandas import DataFrame from skopt.space import Dimension, Integer, Real from freqtrade import OperationalException @@ -42,15 +41,6 @@ class IHyperOpt(ABC): # Assign ticker_interval to be used in hyperopt IHyperOpt.ticker_interval = str(config['ticker_interval']) - @staticmethod - @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. - """ - @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index db51c3ca5..1ad53fe33 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -34,6 +34,9 @@ class HyperOptResolver(IResolver): self.hyperopt = self._load_hyperopt(hyperopt_name, config, extra_dir=config.get('hyperopt_path')) + if not hasattr(self.hyperopt, 'populate_indicators'): + logger.warning("Hyperopt class does not provide populate_indicators() method. " + "Using populate_indicators from the strategy.") if not hasattr(self.hyperopt, 'populate_buy_trend'): logger.warning("Hyperopt class does not provide populate_buy_trend() method. " "Using populate_buy_trend from the strategy.") From 97d0f93d3ce0d4ab3decaa75f7b0432fc320efd4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 11:11:13 +0100 Subject: [PATCH 116/319] Align samples (hyperopt and strategy) to work together --- user_data/hyperopts/sample_hyperopt.py | 33 ++----------------------- user_data/strategies/sample_strategy.py | 15 +++++------ 2 files changed, 10 insertions(+), 38 deletions(-) diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index fabfdb23e..2721ab405 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -2,12 +2,11 @@ from functools import reduce from typing import Any, Callable, Dict, List -from datetime import datetime -import numpy as np +import numpy as np # noqa import talib.abstract as ta from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer, Real # noqa import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -34,34 +33,6 @@ class SampleHyperOpts(IHyperOpt): 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: - """ - 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: diff --git a/user_data/strategies/sample_strategy.py b/user_data/strategies/sample_strategy.py index c2fd681d2..36dea65c9 100644 --- a/user_data/strategies/sample_strategy.py +++ b/user_data/strategies/sample_strategy.py @@ -107,16 +107,16 @@ class SampleStrategy(IStrategy): # RSI dataframe['rsi'] = ta.RSI(dataframe) - """ + # ADX dataframe['adx'] = ta.ADX(dataframe) - + """ # Awesome oscillator dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) # Commodity Channel Index: values Oversold:<-100, Overbought:>100 dataframe['cci'] = ta.CCI(dataframe) - + """ # MACD macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] @@ -126,6 +126,7 @@ class SampleStrategy(IStrategy): # MFI dataframe['mfi'] = ta.MFI(dataframe) + """ # Minus Directional Indicator / Movement dataframe['minus_dm'] = ta.MINUS_DM(dataframe) dataframe['minus_di'] = ta.MINUS_DI(dataframe) @@ -149,12 +150,13 @@ class SampleStrategy(IStrategy): stoch = ta.STOCH(dataframe) dataframe['slowd'] = stoch['slowd'] dataframe['slowk'] = stoch['slowk'] - + """ # Stoch fast stoch_fast = ta.STOCHF(dataframe) dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastk'] = stoch_fast['fastk'] + """ # Stoch RSI stoch_rsi = ta.STOCHRSI(dataframe) dataframe['fastd_rsi'] = stoch_rsi['fastd'] @@ -178,12 +180,11 @@ class SampleStrategy(IStrategy): dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) - # SAR Parabol - dataframe['sar'] = ta.SAR(dataframe) - # SMA - Simple Moving Average dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) """ + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) # TEMA - Triple Exponential Moving Average dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) From 12e86ee4bd48300cf5057ebecc11f7b8b257089c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 11:12:08 +0100 Subject: [PATCH 117/319] Make travis test-hyperopt the sample strategy --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eb171521d..1cc22dfbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ jobs: name: backtest - script: - cp config.json.example config.json - - freqtrade --datadir tests/testdata hyperopt -e 5 + - freqtrade --datadir tests/testdata --strategy SampleStrategy hyperopt --customhyperopt SampleHyperOpts -e 5 name: hyperopt - script: flake8 name: flake8 From 3287cdd47ad5fa042f7f82f85c66a47666aa1530 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 13:01:36 +0100 Subject: [PATCH 118/319] Improve documentation regarding loading methods from hyperopt --- docs/hyperopt.md | 23 +++++++++++++++---- .../hyperopts/sample_hyperopt_advanced.py | 13 +++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 99331707f..3c42a0428 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -23,17 +23,23 @@ Configuring hyperopt is similar to writing your own strategy, and many tasks wil 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 -Optional, but recommended: +!!! Note + `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. +Optional - can also be loaded from a strategy: + +* copy `populate_indicators` from your strategy - otherwise default-strategy will be used * 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 +!!! Note + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + 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) @@ -156,7 +162,7 @@ that minimizes the value of the [loss function](#loss-functions). The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. When you want to test an indicator that isn't used by the bot currently, remember to -add it to the `populate_indicators()` method in `hyperopt.py`. +add it to the `populate_indicators()` method in your custom hyperopt file. ## Loss-functions @@ -270,6 +276,14 @@ For example, to use one month of data, pass the following parameter to the hyper freqtrade hyperopt --timerange 20180401-20180501 ``` +### Running Hyperopt using methods from a strategy + +Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. + +```bash +freqtrade --strategy SampleStrategy hyperopt --customhyperopt SampleHyperopt +``` + ### Running Hyperopt with Smaller Search Space Use the `--spaces` argument to limit the search space used by hyperopt. @@ -341,8 +355,7 @@ So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that t (dataframe['rsi'] < 29.0) ``` -Translating your whole hyperopt result as the new buy-signal -would then look like: +Translating your whole hyperopt result as the new buy-signal would then look like: ```python def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/user_data/hyperopts/sample_hyperopt_advanced.py index 00062a58d..6986854ee 100644 --- a/user_data/hyperopts/sample_hyperopt_advanced.py +++ b/user_data/hyperopts/sample_hyperopt_advanced.py @@ -37,6 +37,9 @@ class AdvancedSampleHyperOpts(IHyperOpt): """ @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + This method can also be loaded from the strategy, if it doesn't exist in the hyperopt class. + """ dataframe['adx'] = ta.ADX(dataframe) macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] @@ -229,8 +232,9 @@ class AdvancedSampleHyperOpts(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 + Based on TA indicators. + Can be a copy of from the strategy, or will be loaded from the strategy. + must align to populate_indicators used (either from this File, or from the strategy) Only used when --spaces does not include buy """ dataframe.loc[ @@ -246,8 +250,9 @@ class AdvancedSampleHyperOpts(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 + Based on TA indicators. + Can be a copy of from the strategy, or will be loaded from the strategy. + must align to populate_indicators used (either from this File, or from the strategy) Only used when --spaces does not include sell """ dataframe.loc[ From 80ad37ad93761b8ab69df1a2f10a3a14b4e6ad85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 14:17:15 +0100 Subject: [PATCH 119/319] Updated plot_indicators test --- tests/optimize/test_hyperopt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 675bbd62e..36902dcfc 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -149,6 +149,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) hyperopt = DefaultHyperOpt + delattr(hyperopt, 'populate_indicators') delattr(hyperopt, 'populate_buy_trend') delattr(hyperopt, 'populate_sell_trend') mocker.patch( @@ -156,8 +157,11 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: MagicMock(return_value=hyperopt(default_conf)) ) x = HyperOptResolver(default_conf, ).hyperopt + assert not hasattr(x, 'populate_indicators') assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_sell_trend') + assert log_has("Hyperopt class does not provide populate_indicators() method. " + "Using populate_indicators from the strategy.", caplog) assert log_has("Hyperopt class does not provide populate_sell_trend() method. " "Using populate_sell_trend from the strategy.", caplog) assert log_has("Hyperopt class does not provide populate_buy_trend() method. " From 6550e1fa992bef966fcffe1703ede0a20851da04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Nov 2019 09:55:38 +0100 Subject: [PATCH 120/319] Change docstring in sampleHyperopt --- user_data/hyperopts/sample_hyperopt_advanced.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/user_data/hyperopts/sample_hyperopt_advanced.py index 6986854ee..c5d28878c 100644 --- a/user_data/hyperopts/sample_hyperopt_advanced.py +++ b/user_data/hyperopts/sample_hyperopt_advanced.py @@ -233,8 +233,9 @@ class AdvancedSampleHyperOpts(IHyperOpt): def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators. - Can be a copy of from the strategy, or will be loaded from the strategy. - must align to populate_indicators used (either from this File, or from the strategy) + Can be a copy of the corresponding method from the strategy, + or will be loaded from the strategy. + Must align to populate_indicators used (either from this File, or from the strategy) Only used when --spaces does not include buy """ dataframe.loc[ @@ -251,8 +252,9 @@ class AdvancedSampleHyperOpts(IHyperOpt): def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators. - Can be a copy of from the strategy, or will be loaded from the strategy. - must align to populate_indicators used (either from this File, or from the strategy) + Can be a copy of the corresponding method from the strategy, + or will be loaded from the strategy. + Must align to populate_indicators used (either from this File, or from the strategy) Only used when --spaces does not include sell """ dataframe.loc[ From 3eca80217c741f226a2dafa2c7db69cac2561de9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Nov 2019 10:18:46 +0100 Subject: [PATCH 121/319] Don't check exchange for Utils commands --- freqtrade/configuration/check_exchange.py | 2 +- tests/test_configuration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index 5d963db47..9e7ce70a9 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -21,7 +21,7 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: and thus is not known for the Freqtrade at all. """ - if (config['runmode'] in [RunMode.PLOT, RunMode.UTIL_NO_EXCHANGE] + if (config['runmode'] in [RunMode.PLOT, RunMode.UTIL_NO_EXCHANGE, RunMode.OTHER] and not config.get('exchange', {}).get('name')): # Skip checking exchange in plot mode, since it requires no exchange return True diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 545dd5df4..6cb6ca758 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -545,7 +545,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test no exchange... default_conf.get('exchange').update({'name': ''}) - default_conf['runmode'] = RunMode.OTHER + default_conf['runmode'] = RunMode.UTIL_EXCHANGE with pytest.raises(OperationalException, match=r'This command requires a configured exchange.*'): check_exchange(default_conf) From 1e44f93c31a5adbc0617f3767574b2a06b9ec56d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Nov 2019 10:38:21 +0100 Subject: [PATCH 122/319] Fix pandas access warning --- freqtrade/optimize/backtesting.py | 3 ++- tests/optimize/test_backtest_detail.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fe31912bc..076857e62 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -245,7 +245,8 @@ class Backtesting: ticker: Dict = {} # Create ticker dict for pair, pair_data in processed.items(): - pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run + pair_data.loc[:, 'buy'] = 0 # cleanup from previous run + pair_data.loc[:, 'sell'] = 0 # cleanup from previous run ticker_data = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 345e423cd..54f4c8796 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -3,7 +3,6 @@ import logging from unittest.mock import MagicMock import pytest -from pandas import DataFrame from freqtrade.data.history import get_timeframe from freqtrade.optimize.backtesting import Backtesting @@ -313,7 +312,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: pair = "UNITTEST/BTC" # Dummy data as we mock the analyze functions - data_processed = {pair: DataFrame()} + data_processed = {pair: frame.copy()} min_date, max_date = get_timeframe({pair: frame}) results = backtesting.backtest( { From 871019c8b9f8e50f98f8f45128690672e44b681d Mon Sep 17 00:00:00 2001 From: Gautier Pialat Date: Tue, 5 Nov 2019 12:08:57 +0100 Subject: [PATCH 123/319] docker doc update about restart policy --- docs/docker.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docker.md b/docs/docker.md index 8a254b749..7b9fce85f 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -170,6 +170,9 @@ docker run -d \ !!! Note All available bot command line parameters can be added to the end of the `docker run` command. +!!! Note + you can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). + ### Monitor your Docker instance You can use the following commands to monitor and manage your container: From f6a66cd3de7a1e1dafc6e85788c37dce5435eaed Mon Sep 17 00:00:00 2001 From: Gautier Pialat Date: Tue, 5 Nov 2019 12:14:39 +0100 Subject: [PATCH 124/319] Fix typo --- docs/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker.md b/docs/docker.md index 7b9fce85f..a06d82bdc 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -171,7 +171,7 @@ docker run -d \ All available bot command line parameters can be added to the end of the `docker run` command. !!! Note - you can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). + You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). ### Monitor your Docker instance From eb0b0350e031bdd1dca7fc9d7d8ef9eaa41fdfe7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 5 Nov 2019 12:39:19 +0100 Subject: [PATCH 125/319] Introduce remove_credentials to remove code duplication --- freqtrade/configuration/__init__.py | 1 + freqtrade/configuration/check_exchange.py | 13 +++++++++++++ freqtrade/optimize/backtesting.py | 10 +++------- freqtrade/optimize/edge_cli.py | 17 +++++++---------- freqtrade/utils.py | 6 ++---- tests/test_configuration.py | 22 +++++++++++++++++----- tests/test_utils.py | 2 +- 7 files changed, 44 insertions(+), 27 deletions(-) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index ac59421a7..63c38d8c5 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,4 +1,5 @@ from freqtrade.configuration.arguments import Arguments # noqa: F401 +from freqtrade.configuration.check_exchange import check_exchange, remove_credentials # 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/check_exchange.py b/freqtrade/configuration/check_exchange.py index 9e7ce70a9..c739de692 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -10,6 +10,19 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) +def remove_credentials(config: Dict[str, Any]): + """ + Removes exchange keys from the configuration and specifies dry-run + Used for backtesting / hyperopt / edge and utils. + Modifies the input dict! + """ + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + config['exchange']['password'] = '' + config['exchange']['uid'] = '' + config['dry_run'] = True + + def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: """ Check if the exchange name in the config file is supported by Freqtrade diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 076857e62..ee3a135d2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,9 +10,10 @@ 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 TimeRange +from freqtrade.configuration import TimeRange, remove_credentials from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -21,7 +22,6 @@ 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__) @@ -57,11 +57,7 @@ class Backtesting: self.config = config # Reset keys for backtesting - self.config['exchange']['key'] = '' - self.config['exchange']['secret'] = '' - self.config['exchange']['password'] = '' - self.config['exchange']['uid'] = '' - self.config['dry_run'] = True + remove_credentials(self.config) self.strategylist: List[IStrategy] = [] self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 1ba6bcc65..5a4543884 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -4,12 +4,13 @@ This module contains the edge backtesting interface """ import logging -from typing import Dict, Any -from tabulate import tabulate -from freqtrade import constants -from freqtrade.edge import Edge +from typing import Any, Dict -from freqtrade.configuration import TimeRange +from tabulate import tabulate + +from freqtrade import constants +from freqtrade.configuration import TimeRange, remove_credentials +from freqtrade.edge import Edge from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver @@ -29,12 +30,8 @@ class EdgeCli: self.config = config # Reset keys for edge - self.config['exchange']['key'] = '' - self.config['exchange']['secret'] = '' - self.config['exchange']['password'] = '' - self.config['exchange']['uid'] = '' + remove_credentials(self.config) self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - self.config['dry_run'] = True self.exchange = Exchange(self.config) self.strategy = StrategyResolver(self.config).strategy diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 630de0f5a..03cf7dabf 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -10,7 +10,7 @@ import rapidjson from tabulate import tabulate from freqtrade import OperationalException -from freqtrade.configuration import Configuration, TimeRange +from freqtrade.configuration import Configuration, TimeRange, remove_credentials from freqtrade.configuration.directory_operations import create_userdata_dir from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, @@ -33,10 +33,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str configuration = Configuration(args, method) config = configuration.get_config() - config['exchange']['dry_run'] = True # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' + remove_credentials(config) return config diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 6cb6ca758..d27cd92f4 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -10,13 +10,13 @@ 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, check_exchange, + remove_credentials, validate_config_consistency) -from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.config_validation import validate_config_schema -from freqtrade.configuration.deprecated_settings import (check_conflicting_settings, - process_deprecated_setting, - process_temporary_deprecated_settings) +from freqtrade.configuration.deprecated_settings import ( + check_conflicting_settings, process_deprecated_setting, + process_temporary_deprecated_settings) from freqtrade.configuration.directory_operations import (create_datadir, create_userdata_dir) from freqtrade.configuration.load_config import load_config_file @@ -551,6 +551,18 @@ def test_check_exchange(default_conf, caplog) -> None: check_exchange(default_conf) +def test_remove_credentials(default_conf, caplog) -> None: + conf = deepcopy(default_conf) + conf['dry_run'] = False + remove_credentials(conf) + + assert conf['dry_run'] is True + assert conf['exchange']['key'] == '' + assert conf['exchange']['secret'] == '' + assert conf['exchange']['password'] == '' + assert conf['exchange']['uid'] == '' + + def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) diff --git a/tests/test_utils.py b/tests/test_utils.py index 7d6b82809..474a842d0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -19,7 +19,7 @@ def test_setup_utils_configuration(): config = setup_utils_configuration(get_args(args), RunMode.OTHER) assert "exchange" in config - assert config['exchange']['dry_run'] is True + assert config['dry_run'] is True assert config['exchange']['key'] == '' assert config['exchange']['secret'] == '' From c8638ce82fae82e9558a292e6c9dc2a9dcbb7d70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 5 Nov 2019 21:03:06 +0100 Subject: [PATCH 126/319] Fix bug where bids_to_ask_delta causes doublebuys The continue must happen irrespective of the outcome of this - otherwise the below BUY will happen anyway. --- freqtrade/freqtradebot.py | 3 +-- tests/test_freqtradebot.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7206e555f..7e9706803 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -319,8 +319,7 @@ class FreqtradeBot: (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 + continue buycount += self.execute_buy(_pair, stake_amount) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0ed8e8a77..f3baff7ce 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3213,6 +3213,8 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, assert trade.open_date is not None assert trade.exchange == 'bittrex' + assert len(Trade.query.all()) == 1 + # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) From b8a6c55b105592bde1095ca90c9cfc283e5ce5cc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2019 07:21:09 +0000 Subject: [PATCH 127/319] Bump arrow from 0.15.2 to 0.15.4 Bumps [arrow](https://github.com/crsmithdev/arrow) from 0.15.2 to 0.15.4. - [Release notes](https://github.com/crsmithdev/arrow/releases) - [Changelog](https://github.com/crsmithdev/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/crsmithdev/arrow/compare/0.15.2...0.15.4) 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 64a43ee62..08838c3d4 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.1346 SQLAlchemy==1.3.10 python-telegram-bot==12.2.0 -arrow==0.15.2 +arrow==0.15.4 cachetools==3.1.1 requests==2.22.0 urllib3==1.25.6 From bc78316aa52d49f0df194e9dc11813b796368db5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2019 07:21:39 +0000 Subject: [PATCH 128/319] Bump flake8 from 3.7.8 to 3.7.9 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.7.8 to 3.7.9. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.7.8...3.7.9) 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 589ca7c54..f346439af 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==1.8.2 -flake8==3.7.8 +flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.0.0 mypy==0.740 From 28f0c00281c3e3fedcef268f1475e329574be461 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2019 07:21:55 +0000 Subject: [PATCH 129/319] Bump pandas from 0.25.2 to 0.25.3 Bumps [pandas](https://github.com/pandas-dev/pandas) from 0.25.2 to 0.25.3. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v0.25.2...v0.25.3) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8d9b4953f..331e3dc67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.17.3 -pandas==0.25.2 +pandas==0.25.3 From 60109aaa1f83923ed424ab1ad1b088bfe13cec21 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2019 08:37:38 +0000 Subject: [PATCH 130/319] Bump ccxt from 1.18.1346 to 1.19.14 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1346 to 1.19.14. - [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.1346...1.19.14) 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 08838c3d4..c11179fbb 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.1346 +ccxt==1.19.14 SQLAlchemy==1.3.10 python-telegram-bot==12.2.0 arrow==0.15.4 From ca77dbe8dab200367c90654e411a7510d224a9cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Nov 2019 19:33:15 +0100 Subject: [PATCH 131/319] Fix UnicodeError in hyperopt output --- freqtrade/optimize/hyperopt.py | 18 ++++++++++-------- tests/optimize/test_hyperopt.py | 6 ++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index c576ea6f8..6ea2f5133 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,9 +4,9 @@ This module contains the hyperopt logic """ +import locale import logging import sys - from collections import OrderedDict from operator import itemgetter from pathlib import Path @@ -14,10 +14,10 @@ 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 +from colorama import init as colorama_init +from joblib import (Parallel, cpu_count, delayed, dump, load, + wrap_non_picklable_objects) from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension @@ -28,8 +28,8 @@ from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver - +from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, + HyperOptResolver) logger = logging.getLogger(__name__) @@ -216,7 +216,7 @@ class Hyperopt: if print_all: print(log_str) else: - print('\n' + log_str) + print(f'\n{log_str}') else: print('.', end='') sys.stdout.flush() @@ -335,7 +335,9 @@ class Hyperopt: return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. ' f'Total profit {total_profit: 11.8f} {stake_cur} ' - f'({profit: 7.2f}Σ%). Avg duration {duration:5.1f} mins.') + f'({profit: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). ' + f'Avg duration {duration:5.1f} mins.' + ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') def get_optimizer(self, dimensions, cpu_count) -> Optimizer: return Optimizer( diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 36902dcfc..23d8a887c 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 +import locale from datetime import datetime from pathlib import Path from unittest.mock import MagicMock, PropertyMock @@ -565,8 +566,9 @@ def test_generate_optimizer(mocker, default_conf) -> None: } response_expected = { 'loss': 1.9840569076926293, - 'results_explanation': ' 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\N{GREEK CAPITAL LETTER SIGMA}%). Avg duration 100.0 mins.' + ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), 'params': optimizer_param, 'total_profit': 0.00023300 } From da57396d071131102c366d9821e13566cf594998 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Nov 2019 06:55:07 +0100 Subject: [PATCH 132/319] Fix UTC handling of timestamp() conversation in fetch_my_trades --- freqtrade/data/btanalysis.py | 6 +++--- freqtrade/data/history.py | 7 +++---- freqtrade/exchange/exchange.py | 19 ++++++++++++++++++- tests/exchange/test_exchange.py | 17 +++++++++++++++-- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 388deb4b3..2f7a234ce 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -7,7 +7,7 @@ from typing import Dict import numpy as np import pandas as pd -import pytz +from datetime import timezone from freqtrade import persistence from freqtrade.misc import json_load @@ -106,8 +106,8 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: "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_date.replace(tzinfo=timezone.utc), + t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None, t.calc_profit(), t.calc_profit_percent(), t.open_rate, t.close_rate, t.amount, (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index b1e4313ca..3dd40d2b4 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -9,12 +9,11 @@ Includes: import logging import operator from copy import deepcopy -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, List, Optional, Tuple import arrow -import pytz from pandas import DataFrame from freqtrade import OperationalException, misc @@ -56,10 +55,10 @@ def trim_dataframe(df: DataFrame, timerange: TimeRange) -> DataFrame: Trim dataframe based on given timerange """ if timerange.starttype == 'date': - start = datetime.fromtimestamp(timerange.startts, tz=pytz.utc) + start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) df = df.loc[df['date'] >= start, :] if timerange.stoptype == 'date': - stop = datetime.fromtimestamp(timerange.stopts, tz=pytz.utc) + stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc) df = df.loc[df['date'] <= stop, :] return df diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 430a2ff54..a198e8cdb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -875,6 +875,22 @@ class Exchange: @retrier def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: + """ + Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id. + The "since" argument passed in is coming from the database and is in UTC, + as timezone-native datetime object. + From the python documentation: + > Naive datetime instances are assumed to represent local time + Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the + transformation from local timezone to UTC. + This works for timezones UTC+ since then the result will contain trades from a few hours + instead of from the last 5 seconds, however fails for UTC- timezones, + since we're then asking for trades with a "since" argument in the future. + + :param order_id order_id: Order-id as given when creating the order + :param pair: Pair the order is for + :param since: datetime object of the order creation time. Assumes object is in UTC. + """ if self._config['dry_run']: return [] if not self.exchange_has('fetchMyTrades'): @@ -882,7 +898,8 @@ class Exchange: try: # Allow 5s offset to catch slight time offsets (discovered in #1185) # since needs to be int in milliseconds - my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000)) + my_trades = self._api.fetch_my_trades( + pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000)) matched_trades = [trade for trade in my_trades if trade['order'] == order_id] return matched_trades diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8a4121d80..1bb643e24 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1586,8 +1586,20 @@ 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): + """ + Crucial part in this test is the "since" calculation. + The "since" argument passed in is coming from the database and is in UTC, + as timezone-native datetime object. + From the python documentation: + > Naive datetime instances are assumed to represent local time + Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the + transformation from local timezone to UTC. + This works for timezones UTC+ since then the result will contain trades from a few hours + instead of from the last 5 seconds, however fails for UTC- timezones, + since we're then asking for trades with a "since" argument in the future. + """ order_id = 'ABCD-ABCD' - since = datetime(2018, 5, 5, tzinfo=timezone.utc) + since = datetime(2018, 5, 5, 0, 0, 0) default_conf["dry_run"] = False mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) api_mock = MagicMock() @@ -1623,7 +1635,8 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name): 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] == 1525478395000 - assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000 + assert api_mock.fetch_my_trades.call_args[0][1] == int(since.replace( + tzinfo=timezone.utc).timestamp() - 5) * 1000 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_trades_for_order', 'fetch_my_trades', From dd47bd04cd905c8822f5c27a285db8d37d72847f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Nov 2019 01:32:08 -0500 Subject: [PATCH 133/319] Move description to correct place --- tests/exchange/test_exchange.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1bb643e24..925a53c95 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1586,18 +1586,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): - """ - Crucial part in this test is the "since" calculation. - The "since" argument passed in is coming from the database and is in UTC, - as timezone-native datetime object. - From the python documentation: - > Naive datetime instances are assumed to represent local time - Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the - transformation from local timezone to UTC. - This works for timezones UTC+ since then the result will contain trades from a few hours - instead of from the last 5 seconds, however fails for UTC- timezones, - since we're then asking for trades with a "since" argument in the future. - """ + order_id = 'ABCD-ABCD' since = datetime(2018, 5, 5, 0, 0, 0) default_conf["dry_run"] = False From b0150d548a741153791fd1baf9e40394f75a38a1 Mon Sep 17 00:00:00 2001 From: Gautier Pialat Date: Fri, 8 Nov 2019 09:37:54 +0100 Subject: [PATCH 134/319] remove not use statement --- docs/installation.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 9180beb40..6edb28481 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -34,7 +34,6 @@ Freqtrade provides a Linux/MacOS script to install all dependencies and help you ```bash git clone git@github.com:freqtrade/freqtrade.git cd freqtrade -git checkout develop ./setup.sh --install ``` From 076ef0407b2057b6c8ad42c63e2ae9760d5ceb73 Mon Sep 17 00:00:00 2001 From: Gautier Pialat Date: Fri, 8 Nov 2019 09:39:06 +0100 Subject: [PATCH 135/319] git branch note explanation --- docs/installation.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation.md b/docs/installation.md index 6edb28481..d373c95d0 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -37,6 +37,7 @@ cd freqtrade ./setup.sh --install ``` + When cloning the repository the default working branch is name `develop`. This branch contains the last features (can be considered as relatively stable thanks to automated tests). The `master` branch contains the code of the last release (done once per month with a one week old snapshot of the `develop` branch to prevent packaging bugs so potentially more stable). !!! Note Windows installation is explained [here](#windows). From bc5c91f6815b20ae64f6b8e8a5e29abac1ab51f2 Mon Sep 17 00:00:00 2001 From: Gautier Pialat Date: Fri, 8 Nov 2019 10:29:00 +0100 Subject: [PATCH 136/319] add missing note block --- docs/installation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index d373c95d0..ce35572c4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -37,7 +37,9 @@ cd freqtrade ./setup.sh --install ``` +!!! Note "Version considerations" When cloning the repository the default working branch is name `develop`. This branch contains the last features (can be considered as relatively stable thanks to automated tests). The `master` branch contains the code of the last release (done once per month with a one week old snapshot of the `develop` branch to prevent packaging bugs so potentially more stable). + !!! Note Windows installation is explained [here](#windows). From 54b63e89f8a687ee9bd488fd010a57dd243ce355 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 8 Nov 2019 17:32:18 +0300 Subject: [PATCH 137/319] Wordings on top of #2495 --- docs/installation.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index ce35572c4..61ce6c7b2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -29,16 +29,18 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. !!! Note - Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case. + Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn and stop if that's not the case. ```bash git clone git@github.com:freqtrade/freqtrade.git cd freqtrade +git checkout master # Optional, see (1) ./setup.sh --install ``` +(1) This command switches the cloned repository to the use of the `master` branch. This command is not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout master`/`git checkout develop` commands. !!! Note "Version considerations" - When cloning the repository the default working branch is name `develop`. This branch contains the last features (can be considered as relatively stable thanks to automated tests). The `master` branch contains the code of the last release (done once per month with a one week old snapshot of the `develop` branch to prevent packaging bugs so potentially more stable). + When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `master` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). !!! Note Windows installation is explained [here](#windows). From 1f042f5e32a92cf93ee721ecece6a5830408d35a Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 8 Nov 2019 19:38:32 +0300 Subject: [PATCH 138/319] Quick start and easy installation sections reworked --- docs/installation.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 61ce6c7b2..1330e0994 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -26,10 +26,20 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E ## Quick start -Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. +Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot. !!! Note - Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn and stop if that's not the case. + Windows installation is explained [here](#windows). + +The easiest way to install and run Freqtrade is to clone the bot GitHub repository and then run the Easy Installation script, if it's available for your platform. + +!!! Note "Version considerations" + When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `master` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). + +!!! Note + Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. + +This can be achieved with the following commands: ```bash git clone git@github.com:freqtrade/freqtrade.git @@ -37,17 +47,11 @@ cd freqtrade git checkout master # Optional, see (1) ./setup.sh --install ``` -(1) This command switches the cloned repository to the use of the `master` branch. This command is not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout master`/`git checkout develop` commands. +(1) This command switches the cloned repository to the use of the `master` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout master`/`git checkout develop` commands. -!!! Note "Version considerations" - When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `master` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). +## Easy Installation Script (Linux/MacOS) -!!! Note - Windows installation is explained [here](#windows). - -## Easy Installation - Linux Script - -If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot. +If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot. ```bash $ ./setup.sh @@ -60,25 +64,25 @@ usage: ** --install ** -This script will install everything you need to run the bot: +With this option, the script will install everything you need to run the bot: * Mandatory software as: `ta-lib` * Setup your virtualenv * Configure your `config.json` file -This script is a combination of `install script` `--reset`, `--config` +This option is a combination of installation tasks, `--reset` and `--config`. ** --update ** -Update parameter will pull the last version of your current branch and update your virtualenv. +This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. ** --reset ** -Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv. +This option will hard reset your branch (only if you are on either `master` or `develop`) and recreate your virtualenv. ** --config ** -Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. +Use this option to configure the `config.json` configuration file. The script will interactively ask you questions to setup your bot and create your `config.json`. ------ From e632720c02bf982f2a4627d2ce8ce002b460ab97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 06:55:16 +0100 Subject: [PATCH 139/319] Allow chaining of pairlists --- freqtrade/freqtradebot.py | 7 +- freqtrade/pairlist/IPairList.py | 65 ++++-------------- freqtrade/pairlist/IPairListFilter.py | 18 ----- freqtrade/pairlist/LowPriceFilter.py | 29 +++++--- freqtrade/pairlist/PrecisionFilter.py | 13 ++-- freqtrade/pairlist/StaticPairList.py | 15 ++-- freqtrade/pairlist/VolumePairList.py | 32 +++++---- freqtrade/pairlist/pairlistmanager.py | 68 +++++++++++++++++++ freqtrade/resolvers/pairlist_resolver.py | 8 ++- .../resolvers/pairlistfilter_resolver.py | 53 --------------- 10 files changed, 143 insertions(+), 165 deletions(-) delete mode 100644 freqtrade/pairlist/IPairListFilter.py create mode 100644 freqtrade/pairlist/pairlistmanager.py delete mode 100644 freqtrade/resolvers/pairlistfilter_resolver.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a8fc6bc7e..9871ffb89 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -20,9 +20,9 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.persistence import Trade -from freqtrade.resolvers import (ExchangeResolver, PairListResolver, - StrategyResolver) +from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType +from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.wallets import Wallets @@ -70,8 +70,7 @@ class FreqtradeBot: # Attach Wallets to Strategy baseclass IStrategy.wallets = self.wallets - pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList') - self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist + self.pairlists = PairListManager(self.exchange, self.config) # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index eb6af9d52..845c5d01f 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -9,25 +9,16 @@ from abc import ABC, abstractmethod from typing import Dict, List from freqtrade.exchange import market_is_active -from freqtrade.pairlist.IPairListFilter import IPairListFilter -from freqtrade.resolvers.pairlistfilter_resolver import PairListFilterResolver logger = logging.getLogger(__name__) class IPairList(ABC): - def __init__(self, freqtrade, config: dict) -> None: - self._freqtrade = freqtrade + def __init__(self, exchange, config, pairlistconfig: dict) -> None: + self._exchange = exchange self._config = config - self._whitelist = self._config['exchange']['pair_whitelist'] - self._blacklist = self._config['exchange'].get('pair_blacklist', []) - self._filters = self._config.get('pairlist', {}).get('filters', {}) - self._pairlistfilters: List[IPairListFilter] = [] - for pl_filter in self._filters.keys(): - self._pairlistfilters.append( - PairListFilterResolver(pl_filter, freqtrade, self._config).pairlistfilter - ) + self._pairlistconfig = pairlistconfig @property def name(self) -> str: @@ -37,22 +28,6 @@ class IPairList(ABC): """ return self.__class__.__name__ - @property - def whitelist(self) -> List[str]: - """ - Has the current whitelist - -> no need to overwrite in subclasses - """ - return self._whitelist - - @property - def blacklist(self) -> List[str]: - """ - Has the current blacklist - -> no need to overwrite in subclasses - """ - return self._blacklist - @abstractmethod def short_desc(self) -> str: """ @@ -61,28 +36,16 @@ class IPairList(ABC): """ @abstractmethod - def refresh_pairlist(self) -> None: + def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: """ - Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary -> Please overwrite in subclasses + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist """ - def validate_whitelist(self, pairlist: List[str], - tickers: List[Dict] = []) -> List[str]: - """ - Validate pairlist against active markets and blacklist. - Run PairlistFilters if these are configured. - """ - pairlist = self._whitelist_for_active_markets(pairlist) - - if not tickers: - # Refresh tickers if they are not used by the parent Pairlist - tickers = self._freqtrade.exchange.get_tickers() - - for pl_filter in self._pairlistfilters: - pairlist = pl_filter.filter_pairlist(pairlist, tickers) - return pairlist - def _whitelist_for_active_markets(self, whitelist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary @@ -90,16 +53,14 @@ class IPairList(ABC): :return: the list of pairs the user wants to trade without those unavailable or black_listed """ - markets = self._freqtrade.exchange.markets + markets = self._exchange.markets sanitized_whitelist: List[str] = [] for pair in whitelist: - # pair is not in the generated dynamic market, or in the blacklist ... ignore it - if (pair in self.blacklist or pair not in markets - or not pair.endswith(self._config['stake_currency'])): + # pair is not in the generated dynamic market or has the wrong stake currency + if (pair not in markets or not pair.endswith(self._config['stake_currency'])): logger.warning(f"Pair {pair} is not compatible with exchange " - f"{self._freqtrade.exchange.name} or contained in " - f"your blacklist. Removing it from whitelist..") + f"{self._exchange.name}. Removing it from whitelist..") continue # Check if market is active market = markets[pair] diff --git a/freqtrade/pairlist/IPairListFilter.py b/freqtrade/pairlist/IPairListFilter.py deleted file mode 100644 index 4b43f0e9f..000000000 --- a/freqtrade/pairlist/IPairListFilter.py +++ /dev/null @@ -1,18 +0,0 @@ -import logging -from abc import ABC, abstractmethod -from typing import Dict, List - -logger = logging.getLogger(__name__) - - -class IPairListFilter(ABC): - - def __init__(self, freqtrade, config: dict) -> None: - self._freqtrade = freqtrade - self._config = config - - @abstractmethod - def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: - """ - Method doing the filtering - """ diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/LowPriceFilter.py index 778c9b4e0..2f4a3be75 100644 --- a/freqtrade/pairlist/LowPriceFilter.py +++ b/freqtrade/pairlist/LowPriceFilter.py @@ -2,18 +2,23 @@ import logging from copy import deepcopy from typing import Dict, List -from freqtrade.pairlist.IPairListFilter import IPairListFilter +from freqtrade.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) -class LowPriceFilter(IPairListFilter): +class LowPriceFilter(IPairList): - def __init__(self, freqtrade, config: dict) -> None: - super().__init__(freqtrade, config) + def __init__(self, exchange, config, pairlistconfig: dict) -> None: + super().__init__(exchange, config, pairlistconfig) - self._low_price_percent = config['pairlist']['filters']['LowPriceFilter'].get( - 'low_price_percent', 0) + self._low_price_percent = pairlistconfig.get('low_price_percent', 0) + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return f"{self.name} - Filtering pairs priced below {self._low_price_percent * 100}%." def _validate_ticker_lowprice(self, ticker) -> bool: """ @@ -22,7 +27,7 @@ class LowPriceFilter(IPairListFilter): :param precision: Precision :return: True if the pair can stay, false if it should be removed """ - precision = self._freqtrade.exchange.markets[ticker['symbol']]['precision']['price'] + precision = self._exchange.markets[ticker['symbol']]['precision']['price'] compare = ticker['last'] + 1 / pow(10, precision) changeperc = (compare - ticker['last']) / ticker['last'] @@ -33,10 +38,14 @@ class LowPriceFilter(IPairListFilter): return True def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: - """ - Method doing the filtering - """ + """ + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist + """ # Copy list since we're modifying this list for p in deepcopy(pairlist): ticker = [t for t in tickers if t['symbol'] == p][0] diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index c720b8e61..0a590bec6 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -2,15 +2,18 @@ import logging from copy import deepcopy from typing import Dict, List -from freqtrade.pairlist.IPairListFilter import IPairListFilter +from freqtrade.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) -class PrecisionFilter(IPairListFilter): +class PrecisionFilter(IPairList): - def __init__(self, freqtrade, config: dict) -> None: - super().__init__(freqtrade, config) + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return f"{self.name} - Filtering untradable pairs." def _validate_precision_filter(self, ticker: dict, stoploss: float) -> bool: """ @@ -35,7 +38,7 @@ class PrecisionFilter(IPairListFilter): def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: """ - Method doing the filtering + Filters and sorts pairlists and assigns and returns them again. """ if self._freqtrade.strategy.stoploss is not None: # Precalculate sanitized stoploss value to avoid recalculation for every pair diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index 074652b25..c10dde5a6 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -5,6 +5,7 @@ Provides lists as configured in config.json """ import logging +from typing import List, Dict from freqtrade.pairlist.IPairList import IPairList @@ -13,8 +14,8 @@ logger = logging.getLogger(__name__) class StaticPairList(IPairList): - def __init__(self, freqtrade, config: dict) -> None: - super().__init__(freqtrade, config) + def __init__(self, exchange, config, pairlistconfig: dict) -> None: + super().__init__(exchange, config, pairlistconfig) def short_desc(self) -> str: """ @@ -23,8 +24,12 @@ class StaticPairList(IPairList): """ return f"{self.name}: {self.whitelist}" - def refresh_pairlist(self) -> None: + def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: """ - Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist """ - self._whitelist = self.validate_whitelist(self._config['exchange']['pair_whitelist']) + return self.validate_whitelist(self._config['exchange']['pair_whitelist']) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 911bb3bda..ff601bc44 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -5,7 +5,7 @@ Provides lists as configured in config.json """ import logging -from typing import List +from typing import List, Dict from cachetools import TTLCache, cached @@ -19,18 +19,17 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] class VolumePairList(IPairList): - def __init__(self, freqtrade, config: dict) -> None: - super().__init__(freqtrade, config) - self._whitelistconf = self._config.get('pairlist', {}).get('config') - if 'number_assets' not in self._whitelistconf: + def __init__(self, exchange, config, pairlistconfig: dict) -> None: + super().__init__(exchange, config, pairlistconfig) + + if 'number_assets' not in self._pairlistconfig: raise OperationalException( f'`number_assets` not specified. Please check your configuration ' 'for "pairlist.config.number_assets"') - self._number_pairs = self._whitelistconf['number_assets'] - self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume') - self._precision_filter = self._whitelistconf.get('precision_filter', True) + self._number_pairs = self._pairlistconfig['number_assets'] + self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') - if not self._freqtrade.exchange.exchange_has('fetchTickers'): + if not self._exchange.exchange_has('fetchTickers'): raise OperationalException( 'Exchange does not support dynamic whitelist.' 'Please edit your config and restart the bot' @@ -45,14 +44,16 @@ class VolumePairList(IPairList): def short_desc(self) -> str: """ Short whitelist method description - used for startup-messages - -> Please overwrite in subclasses """ return f"{self.name} - top {self._whitelistconf['number_assets']} volume pairs." - def refresh_pairlist(self) -> None: + def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: """ - Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively - -> Please overwrite in subclasses + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist """ # Generate dynamic whitelist self._whitelist = self._gen_pair_whitelist( @@ -64,17 +65,18 @@ class VolumePairList(IPairList): Updates the whitelist with with a dynamically generated list :param base_currency: base currency as str :param key: sort key (defaults to 'quoteVolume') + :param tickers: Tickers (from exchange.get_tickers()). :return: List of pairs """ - tickers = self._freqtrade.exchange.get_tickers() + tickers = self._exchange.get_tickers() # check length so that we make sure that '/' is actually in the string tickers = [v for k, v in tickers.items() if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency and v[key] is not None)] sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key]) # Validate whitelist to only have active market pairs - pairs = self.validate_whitelist([s['symbol'] for s in sorted_tickers], tickers) + pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers]) logger.info(f"Searching {self._number_pairs} pairs: {pairs[:self._number_pairs]}") diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py new file mode 100644 index 000000000..9e92fdb6a --- /dev/null +++ b/freqtrade/pairlist/pairlistmanager.py @@ -0,0 +1,68 @@ +""" +Static List provider + +Provides lists as configured in config.json + + """ +import logging +from copy import deepcopy +from typing import List + +from freqtrade.pairlist.IPairList import IPairList +from freqtrade.resolvers import PairListResolver + +logger = logging.getLogger(__name__) + + +class PairListManager(): + + def __init__(self, exchange, config: dict) -> None: + self._exchange = exchange + self._config = config + self._whitelist = self._config['exchange'].get('pair_whitelist') + self._blacklist = self._config['exchange'].get('pair_blacklist', []) + self._pairlists: List[IPairList] = [] + + for pl in self._config.get('pairlists', [{'method': "StaticPairList"}]): + pairl = PairListResolver(pl.get('method'), + exchange, config, + pl.get('config')).pairlist + self._pairlists.append(pairl) + + @property + def whitelist(self) -> List[str]: + """ + Has the current whitelist + """ + return self._whitelist + + @property + def blacklist(self) -> List[str]: + """ + Has the current blacklist + -> no need to overwrite in subclasses + """ + return self._blacklist + + def refresh_pairlist(self) -> None: + """ + Run pairlist through all pairlists. + """ + + pairlist = self._whitelist.copy() + + # tickers should be cached to avoid calling the exchange on each call. + tickers = self._exchange.get_tickers() + for pl in self._pairlists: + pl.filter_pairlist(pairlist, tickers) + + pairlist = self._verify_blacklist(pairlist) + self._whitelist = pairlist + + def _verify_blacklist(self, pairlist: List[str]) -> List[str]: + + for pair in deepcopy(pairlist): + if pair in self.blacklist: + logger.warning(f"Pair {pair} in your blacklist. Removing it from whitelist...") + pairlist.remove(pair) + return pairlist diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 0f23bb3fd..db00f6515 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -20,13 +20,15 @@ class PairListResolver(IResolver): __slots__ = ['pairlist'] - def __init__(self, pairlist_name: str, freqtrade, config: dict) -> None: + def __init__(self, pairlist_name: str, exchange, config: dict, pairlistconfig) -> None: """ Load the custom class from config parameter :param config: configuration dictionary or None """ - self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'freqtrade': freqtrade, - 'config': config}) + self.pairlist = self._load_pairlist(pairlist_name, config, + kwargs={'exchange': exchange, + 'config': config, + 'pairlistconfig': pairlistconfig}) def _load_pairlist( self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList: diff --git a/freqtrade/resolvers/pairlistfilter_resolver.py b/freqtrade/resolvers/pairlistfilter_resolver.py deleted file mode 100644 index bf86d1c6c..000000000 --- a/freqtrade/resolvers/pairlistfilter_resolver.py +++ /dev/null @@ -1,53 +0,0 @@ -# pragma pylint: disable=attribute-defined-outside-init - -""" -This module load custom pairlists -""" -import logging -from pathlib import Path - -from freqtrade import OperationalException -from freqtrade.pairlist.IPairListFilter import IPairListFilter -from freqtrade.resolvers import IResolver - -logger = logging.getLogger(__name__) - - -class PairListFilterResolver(IResolver): - """ - This class contains all the logic to load custom PairList class - """ - - __slots__ = ['pairlistfilter'] - - def __init__(self, pairlist_name: str, freqtrade, config: dict) -> None: - """ - Load the custom class from config parameter - :param config: configuration dictionary or None - """ - self.pairlistfilter = self._load_pairlist(pairlist_name, config, - kwargs={'freqtrade': freqtrade, - 'config': config}) - - def _load_pairlist( - self, pairlistfilter_name: str, config: dict, kwargs: dict) -> IPairListFilter: - """ - Search and loads the specified pairlist. - :param pairlistfilter_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 = self.build_search_paths(config, current_path=current_path, - user_subdir=None, extra_dir=None) - - pairlist = self._load_object(paths=abs_paths, object_type=IPairListFilter, - object_name=pairlistfilter_name, kwargs=kwargs) - if pairlist: - return pairlist - raise OperationalException( - f"Impossible to load PairlistFilter '{pairlistfilter_name}'. This class does not exist " - "or contains Python code errors." - ) From b610e8c7e6b9a2d5f6c007fd446541f773fc688b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 07:05:17 +0100 Subject: [PATCH 140/319] Don't refresh tickers if they are not needed --- freqtrade/pairlist/IPairList.py | 10 +++++++++- freqtrade/pairlist/LowPriceFilter.py | 9 +++++++++ freqtrade/pairlist/PrecisionFilter.py | 9 +++++++++ freqtrade/pairlist/StaticPairList.py | 11 ++++++++++- freqtrade/pairlist/VolumePairList.py | 9 +++++++++ freqtrade/pairlist/pairlistmanager.py | 16 +++++++++++----- 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 845c5d01f..366a49bae 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -5,7 +5,7 @@ Provides lists as configured in config.json """ import logging -from abc import ABC, abstractmethod +from abc import ABC, abstractmethod, abstractproperty from typing import Dict, List from freqtrade.exchange import market_is_active @@ -28,6 +28,14 @@ class IPairList(ABC): """ return self.__class__.__name__ + @abstractproperty + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + @abstractmethod def short_desc(self) -> str: """ diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/LowPriceFilter.py index 2f4a3be75..4d4a92676 100644 --- a/freqtrade/pairlist/LowPriceFilter.py +++ b/freqtrade/pairlist/LowPriceFilter.py @@ -14,6 +14,15 @@ class LowPriceFilter(IPairList): self._low_price_percent = pairlistconfig.get('low_price_percent', 0) + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + def short_desc(self) -> str: """ Short whitelist method description - used for startup-messages diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 0a590bec6..3e68ec607 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -9,6 +9,15 @@ logger = logging.getLogger(__name__) class PrecisionFilter(IPairList): + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + def short_desc(self) -> str: """ Short whitelist method description - used for startup-messages diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index c10dde5a6..54cd21d13 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -17,6 +17,15 @@ class StaticPairList(IPairList): def __init__(self, exchange, config, pairlistconfig: dict) -> None: super().__init__(exchange, config, pairlistconfig) + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + def short_desc(self) -> str: """ Short whitelist method description - used for startup-messages @@ -32,4 +41,4 @@ class StaticPairList(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new whitelist """ - return self.validate_whitelist(self._config['exchange']['pair_whitelist']) + return self._config['exchange']['pair_whitelist'] diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index ff601bc44..27b99ade8 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -38,6 +38,15 @@ class VolumePairList(IPairList): raise OperationalException( f'key {self._sort_key} not in {SORT_VALUES}') + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + def _validate_keys(self, key): return key in SORT_VALUES diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 9e92fdb6a..d3532ee54 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -22,11 +22,12 @@ class PairListManager(): self._whitelist = self._config['exchange'].get('pair_whitelist') self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlists: List[IPairList] = [] - + self._tickers_needed = False for pl in self._config.get('pairlists', [{'method': "StaticPairList"}]): pairl = PairListResolver(pl.get('method'), exchange, config, pl.get('config')).pairlist + self._tickers_needed = pairl.needstickers or self._tickers_needed self._pairlists.append(pairl) @property @@ -52,17 +53,22 @@ class PairListManager(): pairlist = self._whitelist.copy() # tickers should be cached to avoid calling the exchange on each call. - tickers = self._exchange.get_tickers() + tickers = [] + if self._tickers_needed: + tickers = self._exchange.get_tickers() + for pl in self._pairlists: pl.filter_pairlist(pairlist, tickers) - pairlist = self._verify_blacklist(pairlist) + # Validation against blacklist happens after the pairlists to ensure blacklist is respected. + pairlist = self.verify_blacklist(pairlist, self.blacklist) self._whitelist = pairlist - def _verify_blacklist(self, pairlist: List[str]) -> List[str]: + @staticmethod + def verify_blacklist(pairlist: List[str], blacklist: List[str]) -> List[str]: for pair in deepcopy(pairlist): - if pair in self.blacklist: + if pair in blacklist: logger.warning(f"Pair {pair} in your blacklist. Removing it from whitelist...") pairlist.remove(pair) return pairlist From 10595862268288e7cafca1c6ed85996f906b2588 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 07:07:33 +0100 Subject: [PATCH 141/319] Small adjustments --- freqtrade/pairlist/StaticPairList.py | 4 ++-- freqtrade/pairlist/VolumePairList.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index 54cd21d13..e70bd671c 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -5,7 +5,7 @@ Provides lists as configured in config.json """ import logging -from typing import List, Dict +from typing import Dict, List from freqtrade.pairlist.IPairList import IPairList @@ -31,7 +31,7 @@ class StaticPairList(IPairList): Short whitelist method description - used for startup-messages -> Please overwrite in subclasses """ - return f"{self.name}: {self.whitelist}" + return f"{self.name}" def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: """ diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 27b99ade8..ba32c8681 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -5,7 +5,7 @@ Provides lists as configured in config.json """ import logging -from typing import List, Dict +from typing import Dict, List from cachetools import TTLCache, cached @@ -54,7 +54,7 @@ class VolumePairList(IPairList): """ Short whitelist method description - used for startup-messages """ - return f"{self.name} - top {self._whitelistconf['number_assets']} volume pairs." + return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs." def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: """ @@ -65,8 +65,7 @@ class VolumePairList(IPairList): :return: new whitelist """ # Generate dynamic whitelist - self._whitelist = self._gen_pair_whitelist( - self._config['stake_currency'], self._sort_key) + return self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key) @cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: From eaf3fd80c591889d30f882ed110cd2a032b13bef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 07:19:46 +0100 Subject: [PATCH 142/319] Allow blacklist-verification from all pairlists --- freqtrade/pairlist/IPairList.py | 13 ++++++++++++- freqtrade/pairlist/VolumePairList.py | 6 ++++-- freqtrade/pairlist/pairlistmanager.py | 15 +++------------ freqtrade/resolvers/pairlist_resolver.py | 3 ++- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 366a49bae..b666b12f2 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -6,6 +6,7 @@ Provides lists as configured in config.json """ import logging from abc import ABC, abstractmethod, abstractproperty +from copy import deepcopy from typing import Dict, List from freqtrade.exchange import market_is_active @@ -15,8 +16,9 @@ logger = logging.getLogger(__name__) class IPairList(ABC): - def __init__(self, exchange, config, pairlistconfig: dict) -> None: + def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict) -> None: self._exchange = exchange + self._pairlistmanager = pairlistmanager self._config = config self._pairlistconfig = pairlistconfig @@ -54,6 +56,15 @@ class IPairList(ABC): :return: new whitelist """ + @staticmethod + def _verify_blacklist(self, pairlist: List[str]) -> List[str]: + + for pair in deepcopy(pairlist): + if pair in self._pairlistmanager.blacklist: + logger.warning(f"Pair {pair} in your blacklist. Removing it from whitelist...") + pairlist.remove(pair) + return pairlist + def _whitelist_for_active_markets(self, whitelist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index ba32c8681..77bdf472d 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -85,7 +85,9 @@ class VolumePairList(IPairList): sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key]) # Validate whitelist to only have active market pairs pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers]) - - logger.info(f"Searching {self._number_pairs} pairs: {pairs[:self._number_pairs]}") + pairs = self._verify_blacklist(pairs) + # Limit to X number of pairs + pairs = pairs[:self._number_pairs] + logger.info(f"Searching {self._number_pairs} pairs: {pairs}") return pairs diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index d3532ee54..b1681afef 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -5,7 +5,6 @@ Provides lists as configured in config.json """ import logging -from copy import deepcopy from typing import List from freqtrade.pairlist.IPairList import IPairList @@ -25,7 +24,7 @@ class PairListManager(): self._tickers_needed = False for pl in self._config.get('pairlists', [{'method': "StaticPairList"}]): pairl = PairListResolver(pl.get('method'), - exchange, config, + exchange, self, config, pl.get('config')).pairlist self._tickers_needed = pairl.needstickers or self._tickers_needed self._pairlists.append(pairl) @@ -57,18 +56,10 @@ class PairListManager(): if self._tickers_needed: tickers = self._exchange.get_tickers() + # Process all pairlists in chain for pl in self._pairlists: - pl.filter_pairlist(pairlist, tickers) + pairlist = pl.filter_pairlist(pairlist, tickers) # Validation against blacklist happens after the pairlists to ensure blacklist is respected. pairlist = self.verify_blacklist(pairlist, self.blacklist) self._whitelist = pairlist - - @staticmethod - def verify_blacklist(pairlist: List[str], blacklist: List[str]) -> List[str]: - - for pair in deepcopy(pairlist): - if pair in blacklist: - logger.warning(f"Pair {pair} in your blacklist. Removing it from whitelist...") - pairlist.remove(pair) - return pairlist diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index db00f6515..a37d7dc05 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -20,13 +20,14 @@ class PairListResolver(IResolver): __slots__ = ['pairlist'] - def __init__(self, pairlist_name: str, exchange, config: dict, pairlistconfig) -> None: + def __init__(self, pairlist_name: str, exchange, pairlistmanager, config: dict, pairlistconfig) -> None: """ Load the custom class from config parameter :param config: configuration dictionary or None """ self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'exchange': exchange, + 'pairlistmanager': pairlistmanager, 'config': config, 'pairlistconfig': pairlistconfig}) From 31c7189b8b3c2cac88670e7a2204e9771bb24117 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 07:23:34 +0100 Subject: [PATCH 143/319] Verify blacklist correctly --- freqtrade/pairlist/IPairList.py | 14 +++++++++++--- freqtrade/pairlist/pairlistmanager.py | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index b666b12f2..e14ae4187 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -57,14 +57,22 @@ class IPairList(ABC): """ @staticmethod - def _verify_blacklist(self, pairlist: List[str]) -> List[str]: - + def verify_blacklist(self, pairlist: List[str], blacklist: List[str]) -> List[str]: + """ + Verify and remove items from pairlist - returning a filtered pairlist. + """ for pair in deepcopy(pairlist): - if pair in self._pairlistmanager.blacklist: + if pair in blacklist: logger.warning(f"Pair {pair} in your blacklist. Removing it from whitelist...") pairlist.remove(pair) return pairlist + def _verify_blacklist(self, pairlist: List[str]) -> List[str]: + """ + Proxy method to verify_blacklist for easy access for child classes. + """ + return IPairList.verify_blacklist(pairlist, self._pairlistmanager.blacklist) + def _whitelist_for_active_markets(self, whitelist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index b1681afef..ee6b3e37b 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -61,5 +61,6 @@ class PairListManager(): pairlist = pl.filter_pairlist(pairlist, tickers) # Validation against blacklist happens after the pairlists to ensure blacklist is respected. - pairlist = self.verify_blacklist(pairlist, self.blacklist) + pairlist = IPairList.verify_blacklist(pairlist, self.blacklist) + self._whitelist = pairlist From bf69b055ebc8b648095e0dfdbf3b461d80577ae7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 09:07:46 +0100 Subject: [PATCH 144/319] Add name getting --- freqtrade/pairlist/IPairList.py | 2 +- freqtrade/pairlist/LowPriceFilter.py | 4 ++-- freqtrade/pairlist/PrecisionFilter.py | 11 +++++------ freqtrade/pairlist/StaticPairList.py | 4 ++-- freqtrade/pairlist/VolumePairList.py | 4 ++-- freqtrade/pairlist/pairlistmanager.py | 16 ++++++++++++++-- freqtrade/resolvers/pairlist_resolver.py | 3 ++- 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index e14ae4187..ef9ea1d04 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -57,7 +57,7 @@ class IPairList(ABC): """ @staticmethod - def verify_blacklist(self, pairlist: List[str], blacklist: List[str]) -> List[str]: + def verify_blacklist(pairlist: List[str], blacklist: List[str]) -> List[str]: """ Verify and remove items from pairlist - returning a filtered pairlist. """ diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/LowPriceFilter.py index 4d4a92676..9552b56b8 100644 --- a/freqtrade/pairlist/LowPriceFilter.py +++ b/freqtrade/pairlist/LowPriceFilter.py @@ -9,8 +9,8 @@ logger = logging.getLogger(__name__) class LowPriceFilter(IPairList): - def __init__(self, exchange, config, pairlistconfig: dict) -> None: - super().__init__(exchange, config, pairlistconfig) + def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig) self._low_price_percent = pairlistconfig.get('low_price_percent', 0) diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 3e68ec607..b18237b00 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -33,11 +33,10 @@ class PrecisionFilter(IPairList): (already cleaned to be 1 - stoploss) :return: True if the pair can stay, false if it should be removed """ - stop_price = self._freqtrade.get_target_bid(ticker["symbol"], ticker) * stoploss + stop_price = ticker['ask'] * stoploss # Adjust stop-prices to precision - sp = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], stop_price) - stop_gap_price = self._freqtrade.exchange.symbol_price_prec(ticker["symbol"], - stop_price * 0.99) + sp = self._exchange.symbol_price_prec(ticker["symbol"], stop_price) + stop_gap_price = self._exchange.symbol_price_prec(ticker["symbol"], stop_price * 0.99) logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: logger.info(f"Removed {ticker['symbol']} from whitelist, " @@ -49,9 +48,9 @@ class PrecisionFilter(IPairList): """ Filters and sorts pairlists and assigns and returns them again. """ - if self._freqtrade.strategy.stoploss is not None: + if self._config.get('stoploss') is not None: # Precalculate sanitized stoploss value to avoid recalculation for every pair - stoploss = 1 - abs(self._freqtrade.strategy.stoploss) + stoploss = 1 - abs(self._config.get('stoploss')) # Copy list since we're modifying this list for p in deepcopy(pairlist): ticker = [t for t in tickers if t['symbol'] == p][0] diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index e70bd671c..f663f8b02 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -14,8 +14,8 @@ logger = logging.getLogger(__name__) class StaticPairList(IPairList): - def __init__(self, exchange, config, pairlistconfig: dict) -> None: - super().__init__(exchange, config, pairlistconfig) + def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig) @property def needstickers(self) -> bool: diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 77bdf472d..610986a72 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -19,8 +19,8 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] class VolumePairList(IPairList): - def __init__(self, exchange, config, pairlistconfig: dict) -> None: - super().__init__(exchange, config, pairlistconfig) + def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig) if 'number_assets' not in self._pairlistconfig: raise OperationalException( diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index ee6b3e37b..e6b2b6103 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -5,7 +5,7 @@ Provides lists as configured in config.json """ import logging -from typing import List +from typing import Dict, List from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import PairListResolver @@ -44,6 +44,18 @@ class PairListManager(): """ return self._blacklist + @property + def name(self) -> str: + """ + """ + return str([p.name for p in self._pairlists]) + + def short_desc(self) -> List[Dict]: + """ + List of short_desc for each pairlist + """ + return [{p.name: p.short_desc()} for p in self._pairlists] + def refresh_pairlist(self) -> None: """ Run pairlist through all pairlists. @@ -52,7 +64,7 @@ class PairListManager(): pairlist = self._whitelist.copy() # tickers should be cached to avoid calling the exchange on each call. - tickers = [] + tickers: List[Dict] = [] if self._tickers_needed: tickers = self._exchange.get_tickers() diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index a37d7dc05..9e051fa8f 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -20,7 +20,8 @@ class PairListResolver(IResolver): __slots__ = ['pairlist'] - def __init__(self, pairlist_name: str, exchange, pairlistmanager, config: dict, pairlistconfig) -> None: + def __init__(self, pairlist_name: str, exchange, pairlistmanager, + config: dict, pairlistconfig) -> None: """ Load the custom class from config parameter :param config: configuration dictionary or None From 85beb3b6a98dc83c5d6cd405c6e260b7743cd20d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 09:31:17 +0100 Subject: [PATCH 145/319] Fix test --- tests/pairlist/test_pairlist.py | 69 ++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 23c48545e..5c718dfdf 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -7,6 +7,7 @@ import pytest from freqtrade import OperationalException from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.resolvers import PairListResolver +from freqtrade.pairlist.pairlistmanager import PairListManager from tests.conftest import get_patched_freqtradebot, log_has_re # whitelist, blacklist @@ -25,26 +26,41 @@ def whitelist_conf(default_conf): default_conf['exchange']['pair_blacklist'] = [ 'BLK/BTC' ] - default_conf['pairlist'] = {'method': 'StaticPairList', - 'config': {'number_assets': 5}, - 'filters': {}, - } + default_conf['pairlists'] = [ + { + "method": "VolumePairList", + "config": { + "number_assets": 5, + "sort_key": "quoteVolume", + } + }, + ] + return default_conf + +@pytest.fixture(scope="function") +def static_pl_conf(default_conf): + default_conf['pairlists'] = [ + { + "method": "StaticPairList", + }, + ] return default_conf def test_load_pairlist_noexist(mocker, markets, default_conf): - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + bot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + plm = PairListManager(bot.exchange, default_conf) with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " r"This class does not exist or contains Python code errors."): - PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist + PairListResolver('NonexistingPairList', bot.exchange, plm, default_conf, {}) -def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf): +def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): - freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) + freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) freqtradebot.pairlists.refresh_pairlist() @@ -53,34 +69,33 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf): # Ensure all except those in whitelist are removed assert set(whitelist) == set(freqtradebot.pairlists.whitelist) # Ensure config dict hasn't been changed - assert (whitelist_conf['exchange']['pair_whitelist'] == + assert (static_pl_conf['exchange']['pair_whitelist'] == freqtradebot.config['exchange']['pair_whitelist']) -def test_refresh_pairlists(mocker, markets, whitelist_conf): - freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) - - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) +def test_refresh_pairlists(mocker, markets, static_pl_conf): + freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + exchange_has=MagicMock(return_value=True), + markets=PropertyMock(return_value=markets), + ) freqtradebot.pairlists.refresh_pairlist() # List ordered by BaseVolume whitelist = ['ETH/BTC', 'TKN/BTC'] # Ensure all except those in whitelist are removed assert set(whitelist) == set(freqtradebot.pairlists.whitelist) - assert whitelist_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist + assert static_pl_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): - whitelist_conf['pairlist'] = {'method': 'VolumePairList', - 'config': {'number_assets': 5, - 'precision_filter': False} - } mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_tickers=tickers, - exchange_has=MagicMock(return_value=True) + exchange_has=MagicMock(return_value=True), ) - freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) + bot = get_patched_freqtradebot(mocker, whitelist_conf) # Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -88,17 +103,19 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co ) # argument: use the whitelist dynamically by exchange-volume whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC'] - freqtradebot.pairlists.refresh_pairlist() + bot.pairlists.refresh_pairlist() - assert whitelist == freqtradebot.pairlists.whitelist + assert whitelist == bot.pairlists.whitelist + + whitelist_conf['pairlists'] = [{'method': 'VolumePairList', + 'config': {} + } + ] - whitelist_conf['pairlist'] = {'method': 'VolumePairList', - 'config': {} - } with pytest.raises(OperationalException, match=r'`number_assets` not specified. Please check your configuration ' r'for "pairlist.config.number_assets"'): - PairListResolver('VolumePairList', freqtradebot, whitelist_conf).pairlist + PairListManager(bot.exchange, whitelist_conf) def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): From 870966dcd0fc8e1e67b2d7b60e22e765ba8c9898 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 09:42:34 +0100 Subject: [PATCH 146/319] Fix more tests --- freqtrade/pairlist/StaticPairList.py | 2 +- tests/pairlist/test_pairlist.py | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index f663f8b02..83450c4bc 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -41,4 +41,4 @@ class StaticPairList(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new whitelist """ - return self._config['exchange']['pair_whitelist'] + return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 5c718dfdf..ffebaf60f 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -16,6 +16,7 @@ from tests.conftest import get_patched_freqtradebot, log_has_re @pytest.fixture(scope="function") def whitelist_conf(default_conf): default_conf['stake_currency'] = 'BTC' + default_conf['exchange']['name'] = 'binance' default_conf['exchange']['pair_whitelist'] = [ 'ETH/BTC', 'TKN/BTC', @@ -39,13 +40,13 @@ def whitelist_conf(default_conf): @pytest.fixture(scope="function") -def static_pl_conf(default_conf): - default_conf['pairlists'] = [ +def static_pl_conf(whitelist_conf): + whitelist_conf['pairlists'] = [ { "method": "StaticPairList", }, ] - return default_conf + return whitelist_conf def test_load_pairlist_noexist(mocker, markets, default_conf): @@ -73,7 +74,7 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): freqtradebot.config['exchange']['pair_whitelist']) -def test_refresh_pairlists(mocker, markets, static_pl_conf): +def test_refresh_static_pairlist(mocker, markets, static_pl_conf): freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -119,6 +120,10 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + exchange_has=MagicMock(return_value=True), + ) freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty)) @@ -179,13 +184,15 @@ def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None @pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): - whitelist_conf['pairlist']['method'] = pairlist - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + whitelist_conf['pairlists'][0]['method'] = pairlist + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True) + ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - assert freqtrade.pairlists.name == pairlist - assert pairlist in freqtrade.pairlists.short_desc() + assert freqtrade.pairlists.name == str([pairlist]) + assert pairlist in str(freqtrade.pairlists.short_desc()) assert isinstance(freqtrade.pairlists.whitelist, list) assert isinstance(freqtrade.pairlists.blacklist, list) From d7262c0b4e7e1b2b02605da117ccd56c0bcc9da3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 13:40:36 +0100 Subject: [PATCH 147/319] Fix correct ticker type --- freqtrade/pairlist/IPairList.py | 3 +- freqtrade/pairlist/LowPriceFilter.py | 6 ++- freqtrade/pairlist/PrecisionFilter.py | 6 +-- freqtrade/pairlist/StaticPairList.py | 2 +- freqtrade/pairlist/VolumePairList.py | 21 ++++++--- freqtrade/pairlist/pairlistmanager.py | 2 +- tests/pairlist/test_pairlist.py | 68 ++++++++++++++++----------- 7 files changed, 65 insertions(+), 43 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index ef9ea1d04..4a83bc939 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -46,7 +46,7 @@ class IPairList(ABC): """ @abstractmethod - def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ Filters and sorts pairlist and returns the whitelist again. Called on each bot iteration - please use internal caching if necessary @@ -97,5 +97,6 @@ class IPairList(ABC): if pair not in sanitized_whitelist: sanitized_whitelist.append(pair) + sanitized_whitelist = self._verify_blacklist(sanitized_whitelist) # We need to remove pairs that are unknown return sanitized_whitelist diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/LowPriceFilter.py index 9552b56b8..4e1ba52c8 100644 --- a/freqtrade/pairlist/LowPriceFilter.py +++ b/freqtrade/pairlist/LowPriceFilter.py @@ -46,7 +46,7 @@ class LowPriceFilter(IPairList): return False return True - def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ Filters and sorts pairlist and returns the whitelist again. @@ -57,7 +57,9 @@ class LowPriceFilter(IPairList): """ # Copy list since we're modifying this list for p in deepcopy(pairlist): - ticker = [t for t in tickers if t['symbol'] == p][0] + ticker = tickers.get(p) + if not ticker: + pairlist.remove(p) # Filter out assets which would not allow setting a stoploss if self._low_price_percent and not self._validate_ticker_lowprice(ticker): diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index b18237b00..d7b2c96ae 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -44,7 +44,7 @@ class PrecisionFilter(IPairList): return False return True - def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ Filters and sorts pairlists and assigns and returns them again. """ @@ -53,9 +53,9 @@ class PrecisionFilter(IPairList): stoploss = 1 - abs(self._config.get('stoploss')) # Copy list since we're modifying this list for p in deepcopy(pairlist): - ticker = [t for t in tickers if t['symbol'] == p][0] + ticker = tickers.get(p) # Filter out assets which would not allow setting a stoploss - if (stoploss and not self._validate_precision_filter(ticker, stoploss)): + if not ticker or (stoploss and not self._validate_precision_filter(ticker, stoploss)): pairlist.remove(p) continue diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index 83450c4bc..a7b71875c 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -33,7 +33,7 @@ class StaticPairList(IPairList): """ return f"{self.name}" - def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ Filters and sorts pairlist and returns the whitelist again. Called on each bot iteration - please use internal caching if necessary diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 610986a72..e6ff69daf 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -5,10 +5,9 @@ Provides lists as configured in config.json """ import logging +from datetime import datetime from typing import Dict, List -from cachetools import TTLCache, cached - from freqtrade import OperationalException from freqtrade.pairlist.IPairList import IPairList @@ -28,6 +27,7 @@ class VolumePairList(IPairList): 'for "pairlist.config.number_assets"') self._number_pairs = self._pairlistconfig['number_assets'] self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') + self._ttl = self._pairlistconfig.get('ttl', 1800) if not self._exchange.exchange_has('fetchTickers'): raise OperationalException( @@ -37,6 +37,7 @@ class VolumePairList(IPairList): if not self._validate_keys(self._sort_key): raise OperationalException( f'key {self._sort_key} not in {SORT_VALUES}') + self._last_refresh = 0 @property def needstickers(self) -> bool: @@ -56,7 +57,7 @@ class VolumePairList(IPairList): """ return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs." - def filter_pairlist(self, pairlist: List[str], tickers: List[Dict]) -> List[str]: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ Filters and sorts pairlist and returns the whitelist again. Called on each bot iteration - please use internal caching if necessary @@ -65,10 +66,17 @@ class VolumePairList(IPairList): :return: new whitelist """ # Generate dynamic whitelist - return self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key) + if self._last_refresh + self._ttl < datetime.now().timestamp(): + self._last_refresh = datetime.now().timestamp() + return self._gen_pair_whitelist(pairlist, + tickers, + self._config['stake_currency'], + self._sort_key, + ) + else: + return pairlist - @cached(TTLCache(maxsize=1, ttl=1800)) - def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: + def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str, key: str) -> List[str]: """ Updates the whitelist with with a dynamically generated list :param base_currency: base currency as str @@ -77,7 +85,6 @@ class VolumePairList(IPairList): :return: List of pairs """ - tickers = self._exchange.get_tickers() # check length so that we make sure that '/' is actually in the string tickers = [v for k, v in tickers.items() if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index e6b2b6103..03451e725 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -64,7 +64,7 @@ class PairListManager(): pairlist = self._whitelist.copy() # tickers should be cached to avoid calling the exchange on each call. - tickers: List[Dict] = [] + tickers: Dict = {} if self._tickers_needed: tickers = self._exchange.get_tickers() diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index ffebaf60f..5ad7fdf5a 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -16,7 +16,6 @@ from tests.conftest import get_patched_freqtradebot, log_has_re @pytest.fixture(scope="function") def whitelist_conf(default_conf): default_conf['stake_currency'] = 'BTC' - default_conf['exchange']['name'] = 'binance' default_conf['exchange']['pair_whitelist'] = [ 'ETH/BTC', 'TKN/BTC', @@ -137,31 +136,37 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): @pytest.mark.parametrize("filters,base_currency,key,whitelist_result", [ - ({}, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), - ({}, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'HOT/BTC', 'FUEL/BTC']), - ({}, "USDT", "quoteVolume", ['ETH/USDT']), - ({}, "ETH", "quoteVolume", []), - ({"PrecisionFilter": {}}, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC", 'FUEL/BTC']), - ({"PrecisionFilter": {}}, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC", 'FUEL/BTC']), - ({"LowPriceFilter": {"low_price_percent": 0.03}}, "BTC", - "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'FUEL/BTC']), + ([], "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), + ([], "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'HOT/BTC', 'FUEL/BTC']), + ([], "USDT", "quoteVolume", ['ETH/USDT']), + ([], "ETH", "quoteVolume", []), + ([{"method": "PrecisionFilter"}], "BTC", + "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC", 'FUEL/BTC']), + ([{"method": "PrecisionFilter"}], + "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC", 'FUEL/BTC']), + ([{"method": "LowPriceFilter", "config": {"low_price_percent": 0.03}}], + "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'FUEL/BTC']), # Hot is removed by precision_filter, Fuel by low_price_filter. - ({"PrecisionFilter": {}, "LowPriceFilter": {"low_price_percent": 0.02}}, - "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC']), -]) + ([{"method": "PrecisionFilter"}, + {"method": "LowPriceFilter", "config": {"low_price_percent": 0.02}} + ], "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC'])]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, filters, base_currency, key, whitelist_result, caplog) -> None: - whitelist_conf['pairlist']['method'] = 'VolumePairList' - whitelist_conf['pairlist']['filters'] = filters + whitelist_conf['pairlists'].extend(filters) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=shitcoinmarkets)) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) + + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_tickers=tickers, + markets=PropertyMock(return_value=shitcoinmarkets), + ) freqtrade.config['stake_currency'] = base_currency - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key) + freqtrade.pairlists.refresh_pairlist() + whitelist = freqtrade.pairlists.whitelist + assert sorted(whitelist) == sorted(whitelist_result) if 'PrecisionFilter' in filters: assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' @@ -172,11 +177,14 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: - default_conf['pairlist'] = {'method': 'VolumePairList', - 'config': {'number_assets': 10} - } - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) + default_conf['pairlists'] = [{'method': 'VolumePairList', + 'config': {'number_assets': 10} + }] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_tickers=tickers, + exchange_has=MagicMock(return_value=False), + ) with pytest.raises(OperationalException): get_patched_freqtradebot(mocker, default_conf) @@ -202,18 +210,22 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): (['ETH/BTC', 'TKN/BTC'], ""), (['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), # TRX/ETH wrong stake (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BCH/BTC not available - (['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "is not compatible with exchange"), # BLK/BTC in blacklist + (['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "in your blacklist. Removing "), # BLK/BTC in blacklist (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") # BTT/BTC is inactive ]) def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, - log_message): - whitelist_conf['pairlist']['method'] = pairlist - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + log_message, tickers): + whitelist_conf['pairlists'][0]['method'] = pairlist + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) caplog.clear() - new_whitelist = freqtrade.pairlists._whitelist_for_active_markets(whitelist) + # Assign starting whitelist + new_whitelist = freqtrade.pairlists._pairlists[0]._whitelist_for_active_markets(whitelist) assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC']) assert log_message in caplog.text From c3b4a4dde1e2421bbd3db783b463ab6b1e9fbee9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 13:59:19 +0100 Subject: [PATCH 148/319] Update sample configurations --- config.json.example | 5 ++++- config_binance.json.example | 5 +++++ config_full.json.example | 23 ++++++++++++++--------- config_kraken.json.example | 5 +++++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/config.json.example b/config.json.example index 9a6dafd04..a2add358f 100644 --- a/config.json.example +++ b/config.json.example @@ -52,6 +52,9 @@ "DOGE/BTC" ] }, + "pairlists": [ + {"method": "StaticPairList"} + ], "edge": { "enabled": false, "process_throttle_secs": 3600, @@ -68,7 +71,7 @@ "remove_pumps": false }, "telegram": { - "enabled": true, + "enabled": false, "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" }, diff --git a/config_binance.json.example b/config_binance.json.example index 58817a78e..aa36ed035 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -54,6 +54,11 @@ "BNB/BTC" ] }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], "edge": { "enabled": false, "process_throttle_secs": 3600, diff --git a/config_full.json.example b/config_full.json.example index 9cec8fcb8..2bc2cde9e 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -50,17 +50,22 @@ "buy": "gtc", "sell": "gtc" }, - "pairlist": { - "method": "VolumePairList", - "config": { - "number_assets": 20, - "sort_key": "quoteVolume", + "pairlists": [ + {"method": "StaticPairList"}, + { + "method": "VolumePairList", + "config": { + "number_assets": 20, + "sort_key": "quoteVolume" + } }, - "filters":{ - "PrecisionFilter": {}, - "LowPriceFilter": {"low_price_percent": 0.01} + {"method": "PrecisionFilter"}, + {"method": "LowPriceFilter", + "config": { + "low_price_percent": 0.01 + } } - }, + ], "exchange": { "name": "bittrex", "sandbox": false, diff --git a/config_kraken.json.example b/config_kraken.json.example index 5a36941d8..9ca2ca065 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -46,6 +46,11 @@ ] }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], "edge": { "enabled": false, "process_throttle_secs": 3600, From 37985310d5a748302ec0e4d974f66b7efbaddab6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 13:59:35 +0100 Subject: [PATCH 149/319] remove cachetools dependency --- requirements-common.txt | 1 - setup.py | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements-common.txt b/requirements-common.txt index 64a43ee62..4793d25f6 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -4,7 +4,6 @@ ccxt==1.18.1346 SQLAlchemy==1.3.10 python-telegram-bot==12.2.0 arrow==0.15.2 -cachetools==3.1.1 requests==2.22.0 urllib3==1.25.6 wrapt==1.11.2 diff --git a/setup.py b/setup.py index 50b8eee9c..781a5d138 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ setup(name='freqtrade', 'SQLAlchemy', 'python-telegram-bot', 'arrow', - 'cachetools', 'requests', 'urllib3', 'wrapt', From c74d76627546fd2ea84149abc629bde8161e7414 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 14:00:32 +0100 Subject: [PATCH 150/319] move from name to name_list --- freqtrade/pairlist/VolumePairList.py | 2 +- freqtrade/pairlist/pairlistmanager.py | 13 ++++++++++--- freqtrade/rpc/rpc.py | 4 ++-- tests/pairlist/test_pairlist.py | 2 +- tests/rpc/test_rpc.py | 17 ++++++++++------- tests/rpc/test_rpc_apiserver.py | 6 +++--- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index e6ff69daf..902f7abd4 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -67,7 +67,7 @@ class VolumePairList(IPairList): """ # Generate dynamic whitelist if self._last_refresh + self._ttl < datetime.now().timestamp(): - self._last_refresh = datetime.now().timestamp() + self._last_refresh = int(datetime.now().timestamp()) return self._gen_pair_whitelist(pairlist, tickers, self._config['stake_currency'], diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 03451e725..09e024497 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -7,6 +7,7 @@ Provides lists as configured in config.json import logging from typing import Dict, List +from freqtrade import OperationalException from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import PairListResolver @@ -22,12 +23,17 @@ class PairListManager(): self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlists: List[IPairList] = [] self._tickers_needed = False - for pl in self._config.get('pairlists', [{'method': "StaticPairList"}]): + pairlists = self._config.get('pairlists', None) + if not pairlists: + pairlists = [{'method': "StaticPairList"}] + for pl in pairlists: pairl = PairListResolver(pl.get('method'), exchange, self, config, pl.get('config')).pairlist self._tickers_needed = pairl.needstickers or self._tickers_needed self._pairlists.append(pairl) + if not self._pairlists: + raise OperationalException("No Pairlist defined!!") @property def whitelist(self) -> List[str]: @@ -45,10 +51,11 @@ class PairListManager(): return self._blacklist @property - def name(self) -> str: + def name_list(self) -> List[str]: """ + Get list of loaded pairlists names """ - return str([p.name for p in self._pairlists]) + return [p.name for p in self._pairlists] def short_desc(self) -> List[Dict]: """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f994ac006..2de73ac25 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -469,7 +469,7 @@ class RPC: def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" - res = {'method': self._freqtrade.pairlists.name, + res = {'method': self._freqtrade.pairlists.name_list, 'length': len(self._freqtrade.active_pair_whitelist), 'whitelist': self._freqtrade.active_pair_whitelist } @@ -484,7 +484,7 @@ class RPC: and pair not in self._freqtrade.pairlists.blacklist): self._freqtrade.pairlists.blacklist.append(pair) - res = {'method': self._freqtrade.pairlists.name, + res = {'method': self._freqtrade.pairlists.name_list, 'length': len(self._freqtrade.pairlists.blacklist), 'blacklist': self._freqtrade.pairlists.blacklist, } diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 5ad7fdf5a..bca3f4aea 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -199,7 +199,7 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - assert freqtrade.pairlists.name == str([pairlist]) + assert freqtrade.pairlists.name_list == [pairlist] assert pairlist in str(freqtrade.pairlists.short_desc()) assert isinstance(freqtrade.pairlists.whitelist, list) assert isinstance(freqtrade.pairlists.blacklist, list) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index df2261c1f..8747fe6ff 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -719,21 +719,23 @@ def test_rpc_whitelist(mocker, default_conf) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_whitelist() - assert ret['method'] == 'StaticPairList' + assert len(ret['method']) == 1 + assert 'StaticPairList' in ret['method'] assert ret['whitelist'] == default_conf['exchange']['pair_whitelist'] def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: - default_conf['pairlist'] = {'method': 'VolumePairList', - 'config': {'number_assets': 4} - } + default_conf['pairlists'] = [{'method': 'VolumePairList', + 'config': {'number_assets': 4} + }] mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_whitelist() - assert ret['method'] == 'VolumePairList' + assert len(ret['method']) == 1 + assert 'VolumePairList' in ret['method'] assert ret['length'] == 4 assert ret['whitelist'] == default_conf['exchange']['pair_whitelist'] @@ -744,13 +746,14 @@ def test_rpc_blacklist(mocker, default_conf) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_blacklist(None) - assert ret['method'] == 'StaticPairList' + assert len(ret['method']) == 1 + assert 'StaticPairList' in ret['method'] assert len(ret['blacklist']) == 2 assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC'] ret = rpc._rpc_blacklist(["ETH/BTC"]) - assert ret['method'] == 'StaticPairList' + assert 'StaticPairList' in ret['method'] assert len(ret['blacklist']) == 3 assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b572a0514..aa5054314 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -456,7 +456,7 @@ def test_api_blacklist(botclient, mocker): assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], "length": 2, - "method": "StaticPairList"} + "method": ["StaticPairList"]} # Add ETH/BTC to blacklist rc = client_post(client, f"{BASE_URI}/blacklist", @@ -464,7 +464,7 @@ def test_api_blacklist(botclient, mocker): assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], "length": 3, - "method": "StaticPairList"} + "method": ["StaticPairList"]} def test_api_whitelist(botclient): @@ -474,7 +474,7 @@ def test_api_whitelist(botclient): assert_response(rc) assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, - "method": "StaticPairList"} + "method": ["StaticPairList"]} def test_api_forcebuy(botclient, mocker, fee): From 25cb935eeedce1e19afc2383953301b6fe79ab6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 14:15:47 +0100 Subject: [PATCH 151/319] Some more adjustments for new pairlist --- freqtrade/configuration/config_validation.py | 7 ++++--- freqtrade/configuration/configuration.py | 3 +++ freqtrade/configuration/deprecated_settings.py | 11 ++++++++++- tests/conftest.py | 3 +++ tests/rpc/test_rpc_telegram.py | 14 +++++++------- tests/test_configuration.py | 8 +++++--- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 93d93263f..21086c913 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -121,6 +121,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None: if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT]: return - if (conf.get('pairlist', {}).get('method', 'StaticPairList') == 'StaticPairList' - and not conf.get('exchange', {}).get('pair_whitelist')): - raise OperationalException("StaticPairList requires pair_whitelist to be set.") + for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]): + if (pl.get('method') == 'StaticPairList' + and not conf.get('exchange', {}).get('pair_whitelist')): + raise OperationalException("StaticPairList requires pair_whitelist to be set.") diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index be1c7ab4e..b8995edf9 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -81,6 +81,9 @@ class Configuration: if 'ask_strategy' not in config: config['ask_strategy'] = {} + if 'pairlists' not in config: + config['pairlists'] = [] + # validate configuration before returning logger.info('Validating configuration ...') validate_config_schema(config) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 8471028aa..3aec85ae2 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -58,9 +58,18 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', 'experimental', 'ignore_roi_if_buy_signal') + if config.get('pairlist', {}).get("method") == 'VolumePairList': + logger.warning( + "DEPRECATED: " + f"Using VolumePairList in pairlist is deprecated and must be moved to pairlists. " + "Please refer to the docs on configuration details") + config['pairlists'].append({'method': 'VolumePairList', + 'config': config.get('pairlist', {}).get('config') + }) + if config.get('pairlist', {}).get('config', {}).get('precision_filter'): logger.warning( "DEPRECATED: " f"Using precision_filter setting is deprecated and has been replaced by" "PrecisionFilter. Please refer to the docs on configuration details") - config['pairlist'].update({'filters': {'PrecisionFilter': {}}}) + config['pairlists'].append({'method': 'PrecisionFilter'}) diff --git a/tests/conftest.py b/tests/conftest.py index d551596f0..b2c76baec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -242,6 +242,9 @@ def default_conf(testdatadir): "HOT/BTC", ] }, + "pairlists": [ + {"method": "StaticPairList"} + ], "telegram": { "enabled": True, "token": "token", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 766511d2d..bb9d88658 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1050,8 +1050,8 @@ def test_whitelist_static(default_conf, update, mocker) -> None: telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert ('Using whitelist `StaticPairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' - in msg_mock.call_args_list[0][0][0]) + assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" + "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) def test_whitelist_dynamic(default_conf, update, mocker) -> None: @@ -1062,17 +1062,17 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: _send_msg=msg_mock ) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - default_conf['pairlist'] = {'method': 'VolumePairList', - 'config': {'number_assets': 4} - } + default_conf['pairlists'] = [{'method': 'VolumePairList', + 'config': {'number_assets': 4} + }] freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert ('Using whitelist `VolumePairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' - in msg_mock.call_args_list[0][0][0]) + assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" + "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) def test_blacklist_static(default_conf, update, mocker) -> None: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 258088925..6cfae01c6 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -743,9 +743,9 @@ def test_validate_whitelist(default_conf): conf = deepcopy(default_conf) - conf.update({"pairlist": { + conf.update({"pairlists": [{ "method": "VolumePairList", - }}) + }]}) # Dynamic whitelist should not care about pair_whitelist validate_config_consistency(conf) del conf['exchange']['pair_whitelist'] @@ -963,14 +963,16 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca assert default_conf[setting[0]][setting[1]] == setting[5] -def test_process_deprecated_setting_precision_filter(mocker, default_conf, caplog): +def test_process_deprecated_setting_pairlists(mocker, default_conf, caplog): patched_configuration_load_config_file(mocker, default_conf) default_conf.update({'pairlist': { + 'method': 'VolumePairList', 'config': {'precision_filter': True} }}) process_temporary_deprecated_settings(default_conf) assert log_has_re(r'DEPRECATED.*precision_filter.*', caplog) + assert log_has_re(r'DEPRECATED.*in pairlist is deprecated and must be moved*', caplog) def test_check_conflicting_settings(mocker, default_conf, caplog): From ed0c7a6aaf80c4c92df7480577bcbe6d10a66d02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 14:16:11 +0100 Subject: [PATCH 152/319] Update configschema to fit new pairlists approach --- freqtrade/constants.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 5fdd45916..c98c32d4c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -20,7 +20,7 @@ REQUIRED_ORDERTIF = ['buy', 'sell'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] -AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] +AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'LowPriceFilter'] DRY_RUN_WALLET = 999.9 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons @@ -151,13 +151,16 @@ CONF_SCHEMA = { 'block_bad_exchanges': {'type': 'boolean'} } }, - 'pairlist': { - 'type': 'object', - 'properties': { - 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, - 'config': {'type': 'object'} - }, - 'required': ['method'] + 'pairlists': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, + 'config': {'type': 'object'} + }, + 'required': ['method'], + } }, 'telegram': { 'type': 'object', From 02b9da8aba530bc0f9115401f634108d2bdb947f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 14:39:28 +0100 Subject: [PATCH 153/319] Update documentation --- config_full.json.example | 3 ++- docs/configuration.md | 46 ++++++++++++++++++++++++---------------- docs/developer.md | 29 ++++++++++++++----------- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 2bc2cde9e..ba53f47d6 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -56,7 +56,8 @@ "method": "VolumePairList", "config": { "number_assets": 20, - "sort_key": "quoteVolume" + "sort_key": "quoteVolume", + "ttl": 1800 } }, {"method": "PrecisionFilter"}, diff --git a/docs/configuration.md b/docs/configuration.md index 39fb4b8ac..8f62708d1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -82,8 +82,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. | `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation. | `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). +| `pairlists` | StaticPairList | Define one or more pairlists to be used. [More information below](#dynamic-pairlists). | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** | `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** @@ -382,41 +381,50 @@ The valid values are: Pairlists define the list of pairs that the bot should trade. There are [`StaticPairList`](#static-pair-list) and dynamic Whitelists available. -In addition to pairlists, [pairlist filters](#available-pairlist-filters) can be configured, which remove certain assets. -These Filters work with all Pairlist providers and are applied in the sequence they occur. +[`PrecisionFilter`](#precision-filter) and [`LowPriceFilter`](#low-price-pair-filter) act as filters, removing low-value pairs. + +All pairlists can be chained, and a combination of all pairlists will become your new whitelist. + +Inactive markets and blacklisted pairs are always removed from the resulting `pair_whitelist`. ### Available Pairlists * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) +* [`PrecisionFilter`](#precision-filter) +* [`LowPriceFilter`](#low-price-pair-filter) #### Static Pair List -By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. Inactive markets and blacklisted pairs are removed from the pair_whitelist. +By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. +```json +"pairlists": [ + {"method": "StaticPairList"} + ], +``` + #### Volume Pair List `VolumePairList` selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume` and defaults to `quoteVolume`. `VolumePairList` does not consider `pair_whitelist`, but selects the top assets from all available markets (with matching stake-currency) on the exchange. -Pairs in `pair_blacklist` are not considered for `VolumePairList`, even if all other filters would match. + +`ttl` allows setting the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). ```json -"pairlist": { +"pairlists": [{ "method": "VolumePairList", "config": { "number_assets": 20, "sort_key": "quoteVolume", - }, + "ttl": 1800, + } +], ``` -### Available Pairlist Filters - -* [`PrecisionFilter`](#precision-filter) -* [`LowPriceFilter`](#low-price-pair-filter) - #### Precision Filter Filters low-value coins which would not allow setting a stoploss. @@ -440,17 +448,19 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, "pair_whitelist": [], "pair_blacklist": ["BNB/BTC"] }, -"pairlist": { +"pairlists": [ + { "method": "VolumePairList", "config": { "number_assets": 20, "sort_key": "quoteVolume", }, - "filters":{ - "PrecisionFilter": {}, - "LowPriceFilter": {"low_price_percent": 0.01} - } }, + {"method": "PrecisionFilter"}, + {"method": "LowPriceFilter", + "config": {"low_price_percent": 0.01} + } + }], ``` ## Switch to Dry-run mode diff --git a/docs/developer.md b/docs/developer.md index 346578c2e..e63d970e2 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -46,15 +46,18 @@ def test_method_to_test(caplog): The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine. #### Install + * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [docker](https://docs.docker.com/install/) * [docker-compose](https://docs.docker.com/compose/install/) #### Starting the bot ##### Use the develop dockerfile + ``` bash rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml ``` + #### Docker Compose ##### Starting @@ -62,9 +65,11 @@ rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml ``` bash docker-compose up ``` + ![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png) ##### Rebuilding + ``` bash docker-compose build ``` @@ -77,8 +82,8 @@ that can be effected by `docker-compose up` or `docker-compose run freqtrade_dev ``` bash docker-compose exec freqtrade_develop /bin/bash ``` -![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png) +![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png) ## Modules @@ -95,7 +100,7 @@ This is a simple provider, which however serves as a good example on how to star Next, modify the classname of the provider (ideally align this with the Filename). -The base-class provides the an instance of the bot (`self._freqtrade`), as well as the configuration (`self._config`), and initiates both `_blacklist` and `_whitelist`. +The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`) and the pairlist dedicated configuration (`self._pairlistconfig`). ```python self._freqtrade = freqtrade @@ -104,10 +109,9 @@ The base-class provides the an instance of the bot (`self._freqtrade`), as well self._blacklist = self._config['exchange'].get('pair_blacklist', []) ``` - Now, let's step through the methods which require actions: -#### configuration +#### Pairlist configuration Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`. This Pairlist-object may contain a `"config"` dict with additional configurations for the configured pairlist. @@ -120,29 +124,30 @@ Additional elements can be configured as needed. `VolumePairList` uses `"sort_ke Returns a description used for Telegram messages. This should contain the name of the Provider, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`. -#### refresh_pairlist +#### filter_pairlist Override this method and run all calculations needed in this method. This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations. -Assign the resulting whiteslist to `self._whitelist` and `self._blacklist` respectively. These will then be used to run the bot in this iteration. Pairs with open trades will be added to the whitelist to have the sell-methods run correctly. +It get's passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`. -Please also run `self.validate_whitelist(pairs, tickers)` (tickers is optional, but should be passed when you're using tickers anyway) and to check and remove pairs with inactive markets. This function is available in the Parent class (`StaticPairList`) and should ideally not be overwritten. +It must return the resulting pairlist (which may then be passed into the next pairlist filter). + +Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected. ##### sample ``` python - def refresh_pairlist(self) -> None: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: # Generate dynamic whitelist - pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key) - # Validate whitelist to only have active market pairs - self._whitelist = self.validate_whitelist(pairs)[:self._number_pairs] + pairs = self._calculate_pairlist(pairlist, tickers) + return pairs ``` #### _gen_pair_whitelist This is a simple method used by `VolumePairList` - however serves as a good example. -It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider. +In VolumePairList, this implements different methods of sorting, does early validation so only the expected number of pairs is returned. ## Implement a new Exchange (WIP) From a01b34a004baa585cf3a9045d8aeb0374e076a83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 14:44:39 +0100 Subject: [PATCH 154/319] tests --- freqtrade/pairlist/IPairList.py | 4 ++-- tests/pairlist/test_pairlist.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 4a83bc939..fc4187856 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -73,7 +73,7 @@ class IPairList(ABC): """ return IPairList.verify_blacklist(pairlist, self._pairlistmanager.blacklist) - def _whitelist_for_active_markets(self, whitelist: List[str]) -> List[str]: + def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary :param whitelist: the sorted list of pairs the user might want to trade @@ -83,7 +83,7 @@ class IPairList(ABC): markets = self._exchange.markets sanitized_whitelist: List[str] = [] - for pair in whitelist: + for pair in pairlist: # pair is not in the generated dynamic market or has the wrong stake currency if (pair not in markets or not pair.endswith(self._config['stake_currency'])): logger.warning(f"Pair {pair} is not compatible with exchange " diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index bca3f4aea..d19c18715 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -229,3 +229,31 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC']) assert log_message in caplog.text + + +def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf): + whitelist_conf['pairlists'][0]['config'].update({"sort_key": "asdf"}) + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + with pytest.raises(OperationalException, + match=r"key asdf not in .*"): + get_patched_freqtradebot(mocker, whitelist_conf) + + +def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + bot = get_patched_freqtradebot(mocker, whitelist_conf) + assert bot.pairlists._pairlists[0]._last_refresh == 0 + + bot.pairlists.refresh_pairlist() + + assert bot.pairlists._pairlists[0]._last_refresh != 0 + lrf = bot.pairlists._pairlists[0]._last_refresh + bot.pairlists.refresh_pairlist() + # Time should not be updated. + assert bot.pairlists._pairlists[0]._last_refresh == lrf From ae356493665270b557e93099970e78377a8b185d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 14:49:25 +0100 Subject: [PATCH 155/319] improve pairlistmanager errorhandling --- freqtrade/pairlist/pairlistmanager.py | 12 +++++++----- tests/pairlist/test_pairlist.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 09e024497..f29f89abf 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -23,17 +23,19 @@ class PairListManager(): self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlists: List[IPairList] = [] self._tickers_needed = False - pairlists = self._config.get('pairlists', None) - if not pairlists: - pairlists = [{'method': "StaticPairList"}] - for pl in pairlists: + + for pl in self._config.get('pairlists', None): + if 'method' not in pl: + logger.warning(f"No method in {pl}") + continue pairl = PairListResolver(pl.get('method'), exchange, self, config, pl.get('config')).pairlist self._tickers_needed = pairl.needstickers or self._tickers_needed self._pairlists.append(pairl) + if not self._pairlists: - raise OperationalException("No Pairlist defined!!") + raise OperationalException("No Pairlist defined!") @property def whitelist(self) -> List[str]: diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index d19c18715..017f78561 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -257,3 +257,19 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): bot.pairlists.refresh_pairlist() # Time should not be updated. assert bot.pairlists._pairlists[0]._last_refresh == lrf + + +def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): + del whitelist_conf['pairlists'][0]['method'] + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + with pytest.raises(OperationalException, + match=r"No Pairlist defined!"): + get_patched_freqtradebot(mocker, whitelist_conf) + assert log_has_re("No method in .*", caplog) + + whitelist_conf['pairlists'] = [] + + with pytest.raises(OperationalException, + match=r"No Pairlist defined!"): + get_patched_freqtradebot(mocker, whitelist_conf) + From 7ff61f12e99ac67fa61f33e87e06d76e6d303008 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 15:04:04 +0100 Subject: [PATCH 156/319] pass pairlist position into the pairlists --- freqtrade/pairlist/IPairList.py | 11 ++++++++++- freqtrade/pairlist/LowPriceFilter.py | 5 +++-- freqtrade/pairlist/StaticPairList.py | 3 --- freqtrade/pairlist/VolumePairList.py | 22 +++++++++++++++------- freqtrade/pairlist/pairlistmanager.py | 11 +++++++---- freqtrade/resolvers/pairlist_resolver.py | 5 +++-- tests/pairlist/test_pairlist.py | 2 +- 7 files changed, 39 insertions(+), 20 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index fc4187856..231755cb0 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -16,11 +16,20 @@ logger = logging.getLogger(__name__) class IPairList(ABC): - def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict) -> None: + def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict, + pairlist_pos: int) -> None: + """ + :param exchange: Exchange instance + :param pairlistmanager: Instanciating Pairlist manager + :param config: Global bot configuration + :param pairlistconfig: Configuration for this pairlist - can be empty. + :param pairlist_pos: Position of the filter in the pairlist-filter-list + """ self._exchange = exchange self._pairlistmanager = pairlistmanager self._config = config self._pairlistconfig = pairlistconfig + self._pairlist_pos = pairlist_pos @property def name(self) -> str: diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/LowPriceFilter.py index 4e1ba52c8..83b6a85e6 100644 --- a/freqtrade/pairlist/LowPriceFilter.py +++ b/freqtrade/pairlist/LowPriceFilter.py @@ -9,8 +9,9 @@ logger = logging.getLogger(__name__) class LowPriceFilter(IPairList): - def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict) -> None: - super().__init__(exchange, pairlistmanager, config, pairlistconfig) + def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict, + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) self._low_price_percent = pairlistconfig.get('low_price_percent', 0) diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index a7b71875c..0050fbd5c 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -14,9 +14,6 @@ logger = logging.getLogger(__name__) class StaticPairList(IPairList): - def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict) -> None: - super().__init__(exchange, pairlistmanager, config, pairlistconfig) - @property def needstickers(self) -> bool: """ diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 902f7abd4..708c8d7c2 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -18,8 +18,9 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] class VolumePairList(IPairList): - def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict) -> None: - super().__init__(exchange, pairlistmanager, config, pairlistconfig) + def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict, + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) if 'number_assets' not in self._pairlistconfig: raise OperationalException( @@ -85,11 +86,18 @@ class VolumePairList(IPairList): :return: List of pairs """ - # check length so that we make sure that '/' is actually in the string - tickers = [v for k, v in tickers.items() - if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency - and v[key] is not None)] - sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key]) + if self._pairlist_pos == 0: + # If VolumePairList is the first in the list, use fresh pairlist + # check length so that we make sure that '/' is actually in the string + filtered_tickers = [v for k, v in tickers.items() + if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency + and v[key] is not None)] + else: + # If other pairlist is in front, use the incomming pairlist. + filtered_tickers = [v for k, v in tickers.items() if k in pairlist] + + sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key]) + # Validate whitelist to only have active market pairs pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers]) pairs = self._verify_blacklist(pairs) diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index f29f89abf..309ada094 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -23,14 +23,17 @@ class PairListManager(): self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlists: List[IPairList] = [] self._tickers_needed = False - for pl in self._config.get('pairlists', None): if 'method' not in pl: logger.warning(f"No method in {pl}") continue pairl = PairListResolver(pl.get('method'), - exchange, self, config, - pl.get('config')).pairlist + exchange=exchange, + pairlistmanager=self, + config=config, + pairlistconfig=pl.get('config'), + pairlist_pos=len(self._pairlists) + ).pairlist self._tickers_needed = pairl.needstickers or self._tickers_needed self._pairlists.append(pairl) @@ -67,7 +70,7 @@ class PairListManager(): def refresh_pairlist(self) -> None: """ - Run pairlist through all pairlists. + Run pairlist through all configured pairlists. """ pairlist = self._whitelist.copy() diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 9e051fa8f..d849f4ffb 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -21,7 +21,7 @@ class PairListResolver(IResolver): __slots__ = ['pairlist'] def __init__(self, pairlist_name: str, exchange, pairlistmanager, - config: dict, pairlistconfig) -> None: + config: dict, pairlistconfig: dict, pairlist_pos: int) -> None: """ Load the custom class from config parameter :param config: configuration dictionary or None @@ -30,7 +30,8 @@ class PairListResolver(IResolver): kwargs={'exchange': exchange, 'pairlistmanager': pairlistmanager, 'config': config, - 'pairlistconfig': pairlistconfig}) + 'pairlistconfig': pairlistconfig, + 'pairlist_pos': pairlist_pos}) def _load_pairlist( self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList: diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 017f78561..c322dd8c4 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -55,7 +55,7 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " r"This class does not exist or contains Python code errors."): - PairListResolver('NonexistingPairList', bot.exchange, plm, default_conf, {}) + PairListResolver('NonexistingPairList', bot.exchange, plm, default_conf, {}, 1) def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): From 5caeca75096d2e2fd963bcb1645475c818864f96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 15:23:36 +0100 Subject: [PATCH 157/319] Improve tests for pairlist-sequence behaviour --- docs/developer.md | 9 ++--- tests/pairlist/test_pairlist.py | 63 +++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index e63d970e2..c65f429e9 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -100,13 +100,14 @@ This is a simple provider, which however serves as a good example on how to star Next, modify the classname of the provider (ideally align this with the Filename). -The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`) and the pairlist dedicated configuration (`self._pairlistconfig`). +The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`), the pairlist dedicated configuration (`self._pairlistconfig`) and the absolute position within the list of pairlists. ```python - self._freqtrade = freqtrade + self._exchange = exchange + self._pairlistmanager = pairlistmanager self._config = config - self._whitelist = self._config['exchange']['pair_whitelist'] - self._blacklist = self._config['exchange'].get('pair_blacklist', []) + self._pairlistconfig = pairlistconfig + self._pairlist_pos = pairlist_pos ``` Now, let's step through the methods which require actions: diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index c322dd8c4..c40187cc9 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -135,25 +135,44 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): assert set(whitelist) == set(pairslist) -@pytest.mark.parametrize("filters,base_currency,key,whitelist_result", [ - ([], "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), - ([], "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'HOT/BTC', 'FUEL/BTC']), - ([], "USDT", "quoteVolume", ['ETH/USDT']), - ([], "ETH", "quoteVolume", []), - ([{"method": "PrecisionFilter"}], "BTC", - "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC", 'FUEL/BTC']), - ([{"method": "PrecisionFilter"}], - "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC", 'FUEL/BTC']), - ([{"method": "LowPriceFilter", "config": {"low_price_percent": 0.03}}], - "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'FUEL/BTC']), +@pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [ + ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), + # Different sorting depending on quote or bid volume + ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "bidVolume"}}], + "BTC", ['HOT/BTC', 'FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']), + ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}], + "USDT", ['ETH/USDT']), + # No pair for ETH ... + ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}], + "ETH", []), + # Precisionfilter and quote volume + ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}, + {"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']), + # Precisionfilter bid + ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "bidVolume"}}, + {"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']), + # Lowpricefilter and VolumePairList + ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}, + {"method": "LowPriceFilter", "config": {"low_price_percent": 0.03}}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']), # Hot is removed by precision_filter, Fuel by low_price_filter. - ([{"method": "PrecisionFilter"}, + ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}, + {"method": "PrecisionFilter"}, {"method": "LowPriceFilter", "config": {"low_price_percent": 0.02}} - ], "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC'])]) + ], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + # StaticPairlist Only + ([{"method": "StaticPairList"}, + ], "BTC", ['ETH/BTC', 'TKN/BTC']), + # Static Pairlist before VolumePairList - sorting changes + ([{"method": "StaticPairList"}, + {"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "bidVolume"}}, + ], "BTC", ['TKN/BTC', 'ETH/BTC']), +]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, - filters, base_currency, key, whitelist_result, + pairlists, base_currency, whitelist_result, caplog) -> None: - whitelist_conf['pairlists'].extend(filters) + whitelist_conf['pairlists'] = pairlists mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) @@ -167,13 +186,13 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t freqtrade.pairlists.refresh_pairlist() whitelist = freqtrade.pairlists.whitelist - assert sorted(whitelist) == sorted(whitelist_result) - if 'PrecisionFilter' in filters: - assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' - r'would be <= stop limit.*', caplog) - - if 'LowPriceFilter' in filters: - assert log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) + assert whitelist == whitelist_result + for pairlist in pairlists: + if pairlist['method'] == 'PrecisionFilter': + assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' + r'would be <= stop limit.*', caplog) + if pairlist['method'] == 'LowPriceFilter': + assert log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: From 0b4800835c9bfa7f08d2eac5fa228e35bdc06d9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 15:26:30 +0100 Subject: [PATCH 158/319] update documentation --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8f62708d1..a8bfae6f9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -410,7 +410,7 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis `VolumePairList` selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume` and defaults to `quoteVolume`. -`VolumePairList` does not consider `pair_whitelist`, but selects the top assets from all available markets (with matching stake-currency) on the exchange. +`VolumePairList` considers outputs of previous pairlists unless it's the first configured pairlist, it does not consider `pair_whitelist`, but selects the top assets from all available markets (with matching stake-currency) on the exchange. `ttl` allows setting the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). From 86a5dfa62e3d72b4d37cf4589dcae68f88a5e1b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 15:28:36 +0100 Subject: [PATCH 159/319] Update documentation --- docs/configuration.md | 2 +- freqtrade/configuration/config_validation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a8bfae6f9..86acab8d6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -383,7 +383,7 @@ There are [`StaticPairList`](#static-pair-list) and dynamic Whitelists available [`PrecisionFilter`](#precision-filter) and [`LowPriceFilter`](#low-price-pair-filter) act as filters, removing low-value pairs. -All pairlists can be chained, and a combination of all pairlists will become your new whitelist. +All pairlists can be chained, and a combination of all pairlists will become your new whitelist. Pairlists are executed in the sequence they are configured. You should always configure either `StaticPairList` or `DynamicPairList` as starting pairlists. Inactive markets and blacklisted pairs are always removed from the resulting `pair_whitelist`. diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 21086c913..83d58c9e4 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -123,5 +123,5 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None: for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]): if (pl.get('method') == 'StaticPairList' - and not conf.get('exchange', {}).get('pair_whitelist')): + and not conf.get('exchange', {}).get('pair_whitelist')): raise OperationalException("StaticPairList requires pair_whitelist to be set.") From 4b15873ee151bc5a651bce0a0d39d3920f6aeb29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 15:36:11 +0100 Subject: [PATCH 160/319] Simplify examples --- config_binance.json.example | 4 +--- config_kraken.json.example | 4 +--- tests/pairlist/test_pairlist.py | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/config_binance.json.example b/config_binance.json.example index aa36ed035..7d616fe91 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -55,9 +55,7 @@ ] }, "pairlists": [ - { - "method": "StaticPairList" - } + {"method": "StaticPairList"} ], "edge": { "enabled": false, diff --git a/config_kraken.json.example b/config_kraken.json.example index 9ca2ca065..854aeef71 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -47,9 +47,7 @@ ] }, "pairlists": [ - { - "method": "StaticPairList" - } + {"method": "StaticPairList"} ], "edge": { "enabled": false, diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index c40187cc9..0ec6766c2 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -291,4 +291,3 @@ def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): with pytest.raises(OperationalException, match=r"No Pairlist defined!"): get_patched_freqtradebot(mocker, whitelist_conf) - From 12654cb810789031232fa58f25634cf000f4cb5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 16:14:18 +0100 Subject: [PATCH 161/319] Add seperate exchange section in docs --- README.md | 8 +------- docs/configuration.md | 13 ++++--------- docs/data-download.md | 6 ++---- docs/exchanges.md | 35 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 5 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 docs/exchanges.md diff --git a/README.md b/README.md index 6d57dcd89..a1feeab67 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ git checkout develop For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/). - ## Basic Usage ### Bot commands @@ -106,7 +105,7 @@ optional arguments: ### Telegram RPC commands -Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/) +Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/) - `/start`: Starts the trader - `/stop`: Stops the trader @@ -129,11 +128,6 @@ The project is currently setup in two main branches: - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. - `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. -## A note on Binance - -For Binance, please add `"BNB/"` to your blacklist to avoid issues. -Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore. - ## Support ### Help / Slack diff --git a/docs/configuration.md b/docs/configuration.md index e250a81ec..6439c9258 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -357,19 +357,12 @@ For example, to test the order type `FOK` with Kraken, and modify candle_limit t !!! Warning Please make sure to fully understand the impacts of these settings before modifying them. -#### Random notes for other exchanges - -* The Ocean (ccxt id: 'theocean') exchange uses Web3 functionality and requires web3 package to be installed: -```shell -$ pip3 install web3 -``` - ### What values can be used for fiat_display_currency? The `fiat_display_currency` configuration parameter sets the base currency to use for the conversion from coin to fiat in the bot Telegram reports. -The valid values are: +The valid values are:p ```json "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD" @@ -476,11 +469,13 @@ you run it in production mode. "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", ... } - ``` + !!! Note If you have an exchange API key yet, [see our tutorial](/pre-requisite). +You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange. + ### Using proxy with FreqTrade To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration. diff --git a/docs/data-download.md b/docs/data-download.md index f105e7a56..1f03b124a 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -78,10 +78,8 @@ freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --d !!! Warning The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading. -### Historic Kraken data - -The Kraken API does only provide 720 historic candles, which is sufficient for FreqTrade dry-run and live trade modes, but is a problem for backtesting. -To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data. +!!! Note "Kraken user" + Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data. ## Next step diff --git a/docs/exchanges.md b/docs/exchanges.md new file mode 100644 index 000000000..8df76c1ba --- /dev/null +++ b/docs/exchanges.md @@ -0,0 +1,35 @@ +# Exchange-specific Notes + +This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges. + +## Binance + +!!! Tip "Stoploss on Exchange" + Binance is currently the only exchange supporting `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it. + +### Blacklists + +For Binance, please add `"BNB/"` to your blacklist to avoid issues. +Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore. + +### Binance sites + +Binance has been split into 3, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. + +* [binance.com](https://www.binance.com/) - International users - ccxt id: `binance` +* [binance.us](https://www.binance.us/) US based users- ccxt id: `binanceus` +* [binance.je](https://www.binance.je/) Trading FIAT currencies - ccxt id: `binanceje` + +### Kraken + +### Historic Kraken data + +The Kraken API does only provide 720 historic candles, which is sufficient for FreqTrade dry-run and live trade modes, but is a problem for backtesting. +To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data. + +#### Random notes for other exchanges + +* The Ocean (ccxt id: 'theocean') exchange uses Web3 functionality and requires web3 package to be installed: +```shell +$ pip3 install web3 +``` diff --git a/mkdocs.yml b/mkdocs.yml index 2c3f70191..0fd8070f5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,7 @@ nav: - Hyperopt: hyperopt.md - Edge Positioning: edge.md - Utility Subcommands: utils.md + - Exchanges: exchanges.md - FAQ: faq.md - Data Analysis: - Jupyter Notebooks: data-analysis.md From de2d04f06b2221559a6703cfe3c77a3c74ab5257 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 16:24:24 +0100 Subject: [PATCH 162/319] Add note about systemd load location closes #2461 --- docs/advanced-setup.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index e6334d2c1..97d52850c 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -8,6 +8,9 @@ If you do not know what things mentioned here mean, you probably do not need it. Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. +!!! Note + Certain systems (like Raspbian) don't load service unit files from the user directory. In this case, copy `freqtrade.service` into `/etc/systemd/user/` (requires superuser permissions). + After that you can start the daemon with: ```bash From 085aa3084ebc07a4bc5dac098577e38dbb415217 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Nov 2019 19:45:09 +0100 Subject: [PATCH 163/319] Implement ticker caching --- freqtrade/pairlist/pairlistmanager.py | 7 ++++++- requirements-common.txt | 1 + setup.py | 1 + tests/pairlist/test_pairlist.py | 4 +++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 309ada094..0734d7f8f 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -4,6 +4,7 @@ Static List provider Provides lists as configured in config.json """ +from cachetools import TTLCache, cached import logging from typing import Dict, List @@ -68,6 +69,10 @@ class PairListManager(): """ return [{p.name: p.short_desc()} for p in self._pairlists] + @cached(TTLCache(maxsize=1, ttl=1800)) + def _get_cached_tickers(self): + return self._exchange.get_tickers() + def refresh_pairlist(self) -> None: """ Run pairlist through all configured pairlists. @@ -78,7 +83,7 @@ class PairListManager(): # tickers should be cached to avoid calling the exchange on each call. tickers: Dict = {} if self._tickers_needed: - tickers = self._exchange.get_tickers() + tickers = self._get_cached_tickers() # Process all pairlists in chain for pl in self._pairlists: diff --git a/requirements-common.txt b/requirements-common.txt index 52b80d501..c11179fbb 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -4,6 +4,7 @@ ccxt==1.19.14 SQLAlchemy==1.3.10 python-telegram-bot==12.2.0 arrow==0.15.4 +cachetools==3.1.1 requests==2.22.0 urllib3==1.25.6 wrapt==1.11.2 diff --git a/setup.py b/setup.py index 781a5d138..50b8eee9c 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ setup(name='freqtrade', 'SQLAlchemy', 'python-telegram-bot', 'arrow', + 'cachetools', 'requests', 'urllib3', 'wrapt', diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 0ec6766c2..94b2147f5 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -268,12 +268,14 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): ) bot = get_patched_freqtradebot(mocker, whitelist_conf) assert bot.pairlists._pairlists[0]._last_refresh == 0 - + assert tickers.call_count == 0 bot.pairlists.refresh_pairlist() + assert tickers.call_count == 1 assert bot.pairlists._pairlists[0]._last_refresh != 0 lrf = bot.pairlists._pairlists[0]._last_refresh bot.pairlists.refresh_pairlist() + assert tickers.call_count == 1 # Time should not be updated. assert bot.pairlists._pairlists[0]._last_refresh == lrf From eba55c27832240febb5f9ec10651931b04cc686e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2019 19:31:13 +0100 Subject: [PATCH 164/319] Change link --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 0fd8070f5..43d6acc1d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,7 +16,7 @@ nav: - Hyperopt: hyperopt.md - Edge Positioning: edge.md - Utility Subcommands: utils.md - - Exchanges: exchanges.md + - Exchange-specific Notes: exchanges.md - FAQ: faq.md - Data Analysis: - Jupyter Notebooks: data-analysis.md From 692d6afbd9ec1953e713e2947270f4b2080ee755 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Mon, 11 Nov 2019 02:17:41 +0300 Subject: [PATCH 165/319] Minor exchange notes typographical cosmetics --- docs/exchanges.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 8df76c1ba..2cf4ed355 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -16,9 +16,9 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ Binance has been split into 3, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. -* [binance.com](https://www.binance.com/) - International users - ccxt id: `binance` -* [binance.us](https://www.binance.us/) US based users- ccxt id: `binanceus` -* [binance.je](https://www.binance.je/) Trading FIAT currencies - ccxt id: `binanceje` +* [binance.com](https://www.binance.com/) - International users, ccxt id: `binance`. +* [binance.us](https://www.binance.us/) - US based users, ccxt id: `binanceus`. +* [binance.je](https://www.binance.je/) - Binance Jersey, trading fiat currencies. Use ccxt id: `binanceje`. ### Kraken From e810597eec91727b53ac3fe910887972a59850cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2019 07:16:35 +0100 Subject: [PATCH 166/319] Add restricted markets snippet to documentation --- docs/exchanges.md | 24 +++++++++++++++++++++++- docs/faq.md | 6 +----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 8df76c1ba..bb30e3f6d 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -20,7 +20,7 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f * [binance.us](https://www.binance.us/) US based users- ccxt id: `binanceus` * [binance.je](https://www.binance.je/) Trading FIAT currencies - ccxt id: `binanceje` -### Kraken +## Kraken ### Historic Kraken data @@ -33,3 +33,25 @@ To download data for the Kraken exchange, using `--dl-trades` is mandatory, othe ```shell $ pip3 install web3 ``` + +## Bittrex + +### Restricted markets + +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. +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. + +You can get a list of restricted markets by using the following snipptet: + +``` python +import ccxt +ct = ccxt.bittrex() +_ = ct.load_markets() +res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarkets()['result'] if x['IsRestricted']] +print(res) + +``` diff --git a/docs/faq.md b/docs/faq.md index 7fdd54958..3ff668bae 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -48,12 +48,8 @@ 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 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. -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. +Read [the Bittrex section about restricted markets](exchanges.md#Restricted markets) for more information. ### How do I search the bot logs for something? From 04b51a982e4aa6c5d88e699b4b0eab45af5183e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2019 08:55:37 +0100 Subject: [PATCH 167/319] Include warning-message to bittrex explanation --- docs/exchanges.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index bb30e3f6d..0006dfa34 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -42,6 +42,13 @@ 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. + +The warning message will look similar to the following: + +``` output +[...] Message: bittrex {"success":false,"message":"RESTRICTED_MARKET","result":null,"explanation":null}" +``` + 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. From 83067c1edc4ac138c2d87b8b8cfc555f0ab384ff Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Mon, 11 Nov 2019 11:18:43 +0300 Subject: [PATCH 168/319] minor: Fix link in the Faq docs --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 3ff668bae..b9c662085 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -49,7 +49,7 @@ You can use the `/forcesell all` command from Telegram. Currently known to happen for US Bittrex users. -Read [the Bittrex section about restricted markets](exchanges.md#Restricted markets) for more information. +Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information. ### How do I search the bot logs for something? From 661c8251c55ab01de6519be576214b867ca033a9 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Mon, 11 Nov 2019 11:23:29 +0300 Subject: [PATCH 169/319] minor: Exchange notes docs * Formatting (structure of sections) * Cosmetic changes This was not noticed in terms of #2505 --- docs/exchanges.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 0006dfa34..ac742a350 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -27,13 +27,6 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f The Kraken API does only provide 720 historic candles, which is sufficient for FreqTrade dry-run and live trade modes, but is a problem for backtesting. To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data. -#### Random notes for other exchanges - -* The Ocean (ccxt id: 'theocean') exchange uses Web3 functionality and requires web3 package to be installed: -```shell -$ pip3 install web3 -``` - ## Bittrex ### Restricted markets @@ -41,7 +34,7 @@ $ pip3 install web3 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. +If you have restricted pairs in your whitelist, you'll get a warning message in the log on Freqtrade startup for each restricted pair. The warning message will look similar to the following: @@ -49,8 +42,8 @@ The warning message will look similar to the following: [...] Message: bittrex {"success":false,"message":"RESTRICTED_MARKET","result":null,"explanation":null}" ``` -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. +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. You can get a list of restricted markets by using the following snipptet: @@ -62,3 +55,10 @@ res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarket print(res) ``` + +## Random notes for other exchanges + +* The Ocean (ccxt id: 'theocean') exchange uses Web3 functionality and requires web3 package to be installed: +```shell +$ pip3 install web3 +``` From 95492958f9fc74c113a02fbfbc51347f8e0d5e56 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Mon, 11 Nov 2019 11:37:57 +0300 Subject: [PATCH 170/319] wordings --- docs/exchanges.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 2cf4ed355..6e4ddf9a8 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -16,9 +16,9 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ Binance has been split into 3, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. -* [binance.com](https://www.binance.com/) - International users, ccxt id: `binance`. -* [binance.us](https://www.binance.us/) - US based users, ccxt id: `binanceus`. -* [binance.je](https://www.binance.je/) - Binance Jersey, trading fiat currencies. Use ccxt id: `binanceje`. +* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. +* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. +* [binance.je](https://www.binance.je/) - Binance Jersey, trading fiat currencies. Use exchange id: `binanceje`. ### Kraken From 27d81bb68c6cabadfc929c49e3f1f5b1c537af57 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Mon, 11 Nov 2019 12:23:24 +0300 Subject: [PATCH 171/319] minor: More cosmetics on Exchange Notes --- docs/exchanges.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 090d70e1f..5bd283a69 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -24,7 +24,7 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ### Historic Kraken data -The Kraken API does only provide 720 historic candles, which is sufficient for FreqTrade dry-run and live trade modes, but is a problem for backtesting. +The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting. To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data. ## Bittrex @@ -45,7 +45,7 @@ The warning message will look similar to the following: 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. -You can get a list of restricted markets by using the following snipptet: +You can get a list of restricted markets by using the following snippet: ``` python import ccxt @@ -53,12 +53,11 @@ ct = ccxt.bittrex() _ = ct.load_markets() res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarkets()['result'] if x['IsRestricted']] print(res) - ``` ## Random notes for other exchanges -* The Ocean (ccxt id: 'theocean') exchange uses Web3 functionality and requires web3 package to be installed: +* The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed: ```shell $ pip3 install web3 ``` From 0a13f7e1c780d1ebc643f235eb0f0f523d2eeb45 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2019 10:18:58 +0000 Subject: [PATCH 172/319] Bump ccxt from 1.19.14 to 1.19.25 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.19.14 to 1.19.25. - [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.19.14...1.19.25) 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 c11179fbb..33a5d0776 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.19.14 +ccxt==1.19.25 SQLAlchemy==1.3.10 python-telegram-bot==12.2.0 arrow==0.15.4 From c65d217d1eae52a20a5a4834ec6031c621ebc256 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2019 10:19:36 +0000 Subject: [PATCH 173/319] Bump scipy from 1.3.1 to 1.3.2 Bumps [scipy](https://github.com/scipy/scipy) from 1.3.1 to 1.3.2. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.3.1...v1.3.2) Signed-off-by: dependabot-preview[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index f5dae7332..ff8de9cb2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.3.1 +scipy==1.3.2 scikit-learn==0.21.3 scikit-optimize==0.5.2 filelock==3.0.12 From 031157f2153e581496c048e1151a2c3b4916a53a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2019 10:20:30 +0000 Subject: [PATCH 174/319] Bump numpy from 1.17.3 to 1.17.4 Bumps [numpy](https://github.com/numpy/numpy) from 1.17.3 to 1.17.4. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.17.3...v1.17.4) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 331e3dc67..ebf27abd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.17.3 +numpy==1.17.4 pandas==0.25.3 From ff1d36434d7f66e53f81c01bbb5fa5be359f0761 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 5 Nov 2019 15:33:48 +0100 Subject: [PATCH 175/319] Add github actions action --- .github/workflows/ci.yml | 167 ++++++++++++++++++++++++++++++ build_helpers/install_windows.ps1 | 6 ++ build_helpers/publish_docker.sh | 20 ++-- tests/test_docs.sh | 12 +++ 4 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 build_helpers/install_windows.ps1 create mode 100755 tests/test_docs.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..9af0e0c64 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,167 @@ +name: Freqtrade CI + +on: + push: + branches: + - master + - develop + tags: + pull_request: + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-18.04] + python-version: [3.7] + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache_dependencies + uses: actions/cache@v1 + id: cache + with: + path: ~/dependencies/ + key: ${{ runner.os }}-dependencies + + - name: pip cache (linux) + uses: actions/cache@preview + if: startsWith(matrix.os, 'ubuntu') + with: + path: ~/.cache/pip + key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip + + - name: pip cache (macOS) + uses: actions/cache@preview + if: startsWith(matrix.os, 'macOS') + with: + path: ~/Library/Caches/pip + key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip + + - name: TA binary *nix + if: steps.cache.outputs.cache-hit != 'true' + run: | + cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. + + - name: Installation - *nix + run: | + python -m pip install --upgrade pip + export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH + export TA_LIBRARY_PATH=${HOME}/dependencies/lib + export TA_INCLUDE_PATH=${HOME}/dependencies/include + pip install -r requirements-dev.txt + pip install -e . + + - name: Tests + run: | + pytest --random-order --cov=freqtrade --cov-config=.coveragerc + # Allow failure for coveralls + coveralls || true + + - name: Backtesting + run: | + cp config.json.example config.json + freqtrade --datadir tests/testdata backtesting + + - name: Hyperopt + run: | + cp config.json.example config.json + freqtrade --datadir tests/testdata --strategy SampleStrategy hyperopt --customhyperopt SampleHyperOpts -e 5 + + - name: Flake8 + run: | + flake8 + + - name: Mypy + run: | + mypy freqtrade scripts + + - name: Documentation syntax + run: | + ./tests/test_docs.sh + + build_windows: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ windows-latest ] + python-version: [3.7] + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Pip cache (Windows) + uses: actions/cache@preview + if: startsWith(runner.os, 'Windows') + with: + path: ~\AppData\Local\pip\Cache + key: ${{ runner.os }}-pip + restore-keys: ${{ runner.os }}-pip + + - name: Installation + run: | + ./build_helpers/install_windows.ps1 + + - name: Tests + run: | + pytest --random-order --cov=freqtrade --cov-config=.coveragerc + + - name: Backtesting + run: | + cp config.json.example config.json + freqtrade --datadir tests/testdata backtesting + + - name: Hyperopt + run: | + cp config.json.example config.json + freqtrade --datadir tests/testdata --strategy SampleStrategy hyperopt --customhyperopt SampleHyperOpts -e 5 + + - name: Flake8 + run: | + flake8 + + - name: Mypy + run: | + mypy freqtrade scripts + + deploy: + needs: [ build, build_windows ] + runs-on: ubuntu-18.04 + if: github.event_name == 'push' || 1 == 1 + steps: + - uses: actions/checkout@v1 + + - name: Build and test and push docker image + env: + IMAGE_NAME: freqtradeorg/freqtradetests + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + # original filter + # branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) + run: | + build_helpers/publish_docker.sh + + - name: Build raspberry image + uses: elgohr/Publish-Docker-Github-Action@2.7 + with: + name: freqtradeorg/freqtradetests:test_pi + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + dockerfile: Dockerfile.pi + cache: ${{ github.event_name != 'cron' }} + tag_names: true + diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 new file mode 100644 index 000000000..e897cb88c --- /dev/null +++ b/build_helpers/install_windows.ps1 @@ -0,0 +1,6 @@ +Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/g5apjq5m/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" + +pip install TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl + +pip install -r requirements-dev.txt +pip install -e . diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 839ca0876..cac2a4c04 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -1,17 +1,17 @@ #!/bin/sh -# - export TAG=`if [ "$TRAVIS_BRANCH" == "develop" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` -# Replace / with _ to create a valid tag -TAG=$(echo "${TRAVIS_BRANCH}" | sed -e "s/\//_/") +# Replace / with _ to create a valid tag +TAG=$(echo "${GITHUB_REF}" | sed -e "s/\//_/g") +echo "Running for ${TAG}" # Add commit and commit_message to docker container -echo "${TRAVIS_COMMIT} ${TRAVIS_COMMIT_MESSAGE}" > freqtrade_commit +echo "${GITHUB_SHA}" > freqtrade_commit -if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then - echo "event ${TRAVIS_EVENT_TYPE}: full rebuild - skipping cache" +if [ "${GITHUB_EVENT_NAME}" = "cron" ]; then + echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache" docker build -t freqtrade:${TAG} . else - echo "event ${TRAVIS_EVENT_TYPE}: building with cache" + echo "event ${GITHUB_EVENT_NAME}: building with cache" # Pull last build to avoid rebuilding the whole image docker pull ${IMAGE_NAME}:${TAG} docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . @@ -23,7 +23,7 @@ if [ $? -ne 0 ]; then fi # Run backtest -docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} --datadir /tests/testdata backtesting +docker run --rm -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} --datadir /tests/testdata backtesting if [ $? -ne 0 ]; then echo "failed running backtest" @@ -38,12 +38,12 @@ if [ $? -ne 0 ]; then fi # Tag as latest for develop builds -if [ "${TRAVIS_BRANCH}" = "develop" ]; then +if [ "${GITHUB_REF}" = "develop" ]; then docker tag freqtrade:$TAG ${IMAGE_NAME}:latest fi # Login -echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD if [ $? -ne 0 ]; then echo "failed login" diff --git a/tests/test_docs.sh b/tests/test_docs.sh new file mode 100755 index 000000000..09e142b99 --- /dev/null +++ b/tests/test_docs.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Test Documentation boxes - +# !!! : is not allowed! +# !!! "title" - Title needs to be quoted! +grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/* + +if [ $? -ne 0 ]; then + echo "Docs test success." + exit 0 +fi +echo "Docs test failed." +exit 1 From e51a7201934db8873244110b1d5d408169ac5d10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2019 10:02:26 +0100 Subject: [PATCH 176/319] Apply cache to pi image --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9af0e0c64..aa2d09750 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,7 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} dockerfile: Dockerfile.pi + # cache: true cache: ${{ github.event_name != 'cron' }} tag_names: true From d1729a624d99b8d509813cea2d2421eff11457ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2019 10:36:38 +0100 Subject: [PATCH 177/319] fix windows build --- .github/workflows/ci.yml | 15 +++++++++++---- .../TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl | Bin 0 -> 684920 bytes build_helpers/install_windows.ps1 | 6 ++++-- build_helpers/publish_docker.sh | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 build_helpers/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa2d09750..4edd357bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - master - develop + - github_actions_2 tags: pull_request: @@ -14,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-18.04] + os: [ ubuntu-18.04 ] python-version: [3.7] steps: @@ -141,24 +142,30 @@ jobs: deploy: needs: [ build, build_windows ] runs-on: ubuntu-18.04 - if: github.event_name == 'push' || 1 == 1 + if: github.event_name == 'push' || github.event_name == 'cron' steps: - uses: actions/checkout@v1 + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + - name: Build and test and push docker image env: IMAGE_NAME: freqtradeorg/freqtradetests DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} # original filter # branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) run: | build_helpers/publish_docker.sh - - name: Build raspberry image + - name: Build raspberry image for ${{ steps.extract_branch.outputs.branch }}_pi uses: elgohr/Publish-Docker-Github-Action@2.7 with: - name: freqtradeorg/freqtradetests:test_pi + name: freqtradeorg/freqtradetests:${{ steps.extract_branch.outputs.branch }}_pi username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} dockerfile: Dockerfile.pi diff --git a/build_helpers/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl new file mode 100644 index 0000000000000000000000000000000000000000..87469a199037a6ce79f4ee4ec347f0c411b8bb49 GIT binary patch literal 684920 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≪c7F=`hCoL#u}a7*VA+qt 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

    e}jSYxSKLA#+4<_BX0`|L| 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

    I_c5
    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

    SDtZ;i8C_#Z`^9CH+x{5WRg57UIK} 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 z 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 literal 0 HcmV?d00001 diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index e897cb88c..30427c3cc 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -1,6 +1,8 @@ -Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/g5apjq5m/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" +# Downloads don't work automatically, since the URL is regenerated via javascript. +# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib +# Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -pip install TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl +pip install build_helpers\TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl pip install -r requirements-dev.txt pip install -e . diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index cac2a4c04..e87047fb0 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -1,7 +1,7 @@ #!/bin/sh # Replace / with _ to create a valid tag -TAG=$(echo "${GITHUB_REF}" | sed -e "s/\//_/g") +TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") echo "Running for ${TAG}" # Add commit and commit_message to docker container From a241c2af0d4a8f73084896217f3e69a247429cae Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2019 19:42:05 +0100 Subject: [PATCH 178/319] Build macos - ... --- .github/workflows/ci.yml | 10 +++++----- build_helpers/publish_docker.sh | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4edd357bf..28ff5fa8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,8 @@ on: - github_actions_2 tags: pull_request: + schedule: + - cron: '0 5 * * 4' jobs: build: @@ -15,7 +17,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-18.04 ] + os: [ ubuntu-18.04, macos-latest ] python-version: [3.7] steps: @@ -142,7 +144,7 @@ jobs: deploy: needs: [ build, build_windows ] runs-on: ubuntu-18.04 - if: github.event_name == 'push' || github.event_name == 'cron' + if: github.event_name == 'push' || github.event_name == 'schedule' steps: - uses: actions/checkout@v1 @@ -157,8 +159,6 @@ jobs: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} - # original filter - # branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) run: | build_helpers/publish_docker.sh @@ -170,6 +170,6 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} dockerfile: Dockerfile.pi # cache: true - cache: ${{ github.event_name != 'cron' }} + cache: ${{ github.event_name != 'schedule' }} tag_names: true diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index e87047fb0..c536e8798 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -7,7 +7,7 @@ echo "Running for ${TAG}" # Add commit and commit_message to docker container echo "${GITHUB_SHA}" > freqtrade_commit -if [ "${GITHUB_EVENT_NAME}" = "cron" ]; then +if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache" docker build -t freqtrade:${TAG} . else From 75d5ff69ef78c30f63c061d0012f1ae9c822703c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2019 20:09:58 +0100 Subject: [PATCH 179/319] Add ping endpoing --- docs/rest-api.md | 9 ++++++++- freqtrade/rpc/api_server.py | 9 +++++++++ tests/rpc/test_rpc_apiserver.py | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 4d5bc5730..85435e0db 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -22,7 +22,14 @@ Sample configuration: !!! Danger "Password selection" Please make sure to select a very strong, unique password to protect your bot from unauthorized access. -You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. +You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` to check if the API is running correctly. +This should return the response: + +``` output +{"status":"pong"} +``` + +All other endpoints will require authentication, so are not available through a webrowser. To generate a secure password, either use a password manager, or use the below code snipped. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 67bbfdc78..5167823fd 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -169,6 +169,8 @@ class ApiServer(RPC): view_func=self._status, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/version', 'version', view_func=self._version, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/ping', 'ping', + view_func=self._ping, methods=['GET']) # Combined actions and infos self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, @@ -224,6 +226,13 @@ class ApiServer(RPC): msg = self._rpc_stopbuy() return self.rest_dump(msg) + @rpc_catch_errors + def _ping(self): + """ + simple poing version + """ + return self.rest_dump({"status": "pong"}) + @require_login @rpc_catch_errors def _version(self): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b572a0514..6e65cf934 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -64,6 +64,10 @@ def test_api_not_found(botclient): def test_api_unauthorized(botclient): ftbot, client = botclient + rc = client.get(f"{BASE_URI}/ping") + assert_response(rc) + assert rc.json == {'status': 'pong'} + # Don't send user/pass information rc = client.get(f"{BASE_URI}/version") assert_response(rc, 401) From 800997437a521cf7ab2923f03b354ce0ca5c5152 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2019 20:25:44 +0100 Subject: [PATCH 180/319] Update documentation --- docs/rest-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 85435e0db..00c2d8e39 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -22,14 +22,14 @@ Sample configuration: !!! Danger "Password selection" Please make sure to select a very strong, unique password to protect your bot from unauthorized access. -You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` to check if the API is running correctly. +You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly. This should return the response: ``` output {"status":"pong"} ``` -All other endpoints will require authentication, so are not available through a webrowser. +All other endpoints return sensitive info and require authentication, so are not available through a webrowser. To generate a secure password, either use a password manager, or use the below code snipped. From 025350ebff08351f29c1171b4057ed4a391658f4 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Tue, 12 Nov 2019 00:07:27 +0300 Subject: [PATCH 181/319] Fix typo in the rest-api docs --- docs/rest-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 00c2d8e39..d0e4c8247 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -29,7 +29,7 @@ This should return the response: {"status":"pong"} ``` -All other endpoints return sensitive info and require authentication, so are not available through a webrowser. +All other endpoints return sensitive info and require authentication, so are not available through a web browser. To generate a secure password, either use a password manager, or use the below code snipped. From 52e24c3a257c5ddd53c255516d578205a5b1277a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 09:27:53 +0100 Subject: [PATCH 182/319] Split error-messsage between incompatible and wrong stake amount --- freqtrade/pairlist/IPairList.py | 7 ++++++- tests/pairlist/test_pairlist.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 231755cb0..d722e70f5 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -94,10 +94,15 @@ class IPairList(ABC): sanitized_whitelist: List[str] = [] for pair in pairlist: # pair is not in the generated dynamic market or has the wrong stake currency - if (pair not in markets or not pair.endswith(self._config['stake_currency'])): + if pair not in markets: logger.warning(f"Pair {pair} is not compatible with exchange " f"{self._exchange.name}. Removing it from whitelist..") continue + if not pair.endswith(self._config['stake_currency']): + logger.warning(f"Pair {pair} is not compatible with your stake currency " + f"{self._config['stake_currency']}. Removing it from whitelist..") + continue + # Check if market is active market = markets[pair] if not market_is_active(market): diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 94b2147f5..13f868c7a 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -227,10 +227,16 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): @pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) @pytest.mark.parametrize("whitelist,log_message", [ (['ETH/BTC', 'TKN/BTC'], ""), - (['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), # TRX/ETH wrong stake - (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BCH/BTC not available - (['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "in your blacklist. Removing "), # BLK/BTC in blacklist - (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") # BTT/BTC is inactive + # TRX/ETH not in markets + (['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), + # wrong stake + (['ETH/BTC', 'TKN/BTC', 'ETH/USDT'], "is not compatible with your stake currency"), + # BCH/BTC not available + (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), + # BLK/BTC in blacklist + (['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "in your blacklist. Removing "), + # BTT/BTC is inactive + (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") ]) def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, log_message, tickers): From 37ef5c38f0309bca902f0a18e312395b94bc2edd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 09:39:30 +0100 Subject: [PATCH 183/319] integrate Slack notification --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ .travis.yml | 10 +++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28ff5fa8a..015585af1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,17 @@ jobs: run: | ./tests/test_docs.sh + - name: Slack Notification + uses: homoluctus/slatify@v1.8.0 + if: always() + with: + type: ${{ job.status }} + job_name: '*Freqtrade CI ${{ matrix.os }}*' + mention: 'here' + mention_if: 'failure' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + build_windows: runs-on: ${{ matrix.os }} @@ -141,6 +152,17 @@ jobs: run: | mypy freqtrade scripts + - name: Slack Notification + uses: homoluctus/slatify@v1.8.0 + if: always() + with: + type: ${{ job.status }} + job_name: '*Freqtrade CI windows*' + mention: 'here' + mention_if: 'failure' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + deploy: needs: [ build, build_windows ] runs-on: ubuntu-18.04 @@ -173,3 +195,14 @@ jobs: cache: ${{ github.event_name != 'schedule' }} tag_names: true + - name: Slack Notification + uses: homoluctus/slatify@v1.8.0 + if: always() + with: + type: ${{ job.status }} + job_name: '*Freqtrade CI Deploy*' + mention: 'here' + mention_if: 'failure' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + diff --git a/.travis.yml b/.travis.yml index 1cc22dfbd..ca35ef4ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,11 +45,11 @@ jobs: - script: mypy freqtrade scripts name: mypy - - stage: docker - if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) - script: - - build_helpers/publish_docker.sh - name: "Build and test and push docker image" + # - stage: docker + # if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) + # script: + # - build_helpers/publish_docker.sh + # name: "Build and test and push docker image" notifications: slack: From 96f550c6aab6081666eacda17c100d114c11bdad Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 10:35:36 +0100 Subject: [PATCH 184/319] Disable tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 015585af1..f7da23156 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: branches: - master - develop - - github_actions_2 + - github_actions_tests tags: pull_request: schedule: From 8c76f45030d74ff856f089561a121ec91d03ad46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 10:53:48 +0100 Subject: [PATCH 185/319] Use correct dockerhub image name --- .github/workflows/ci.yml | 4 ++-- docs/developer.md | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7da23156..67862282a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,7 +177,7 @@ jobs: - name: Build and test and push docker image env: - IMAGE_NAME: freqtradeorg/freqtradetests + IMAGE_NAME: freqtradeorg/freqtrade DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} @@ -187,7 +187,7 @@ jobs: - name: Build raspberry image for ${{ steps.extract_branch.outputs.branch }}_pi uses: elgohr/Publish-Docker-Github-Action@2.7 with: - name: freqtradeorg/freqtradetests:${{ steps.extract_branch.outputs.branch }}_pi + name: freqtradeorg/freqtrade:${{ steps.extract_branch.outputs.branch }}_pi username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} dockerfile: Dockerfile.pi diff --git a/docs/developer.md b/docs/developer.md index 67c0912a7..41c66ce37 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -198,6 +198,19 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace user_data/not jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md ``` +## Continuous integration + +This documents some decisions taken for the CI Pipeline. + +* CI runs on all OS variants, Linux (ubuntu), macOS and Windows. +* Docker images are build for the branches `master` and `develop`. +* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:master_pi` and `develop_pi`. +* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of. +* Full docker image rebuilds are run once a week via schedule. +* Deployments run on ubuntu. +* ta-lib binaries are contained in the build_helpers directory to avoid fails related to external unavailability. +* All tests must pass for a PR to be merged to `master` or `develop`. + ## Creating a release This part of the documentation is aimed at maintainers, and shows how to create a release. From e8a8f416f3b1a076bc668b724339143fcb373521 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 20:08:49 +0200 Subject: [PATCH 186/319] Update dockerhub description from github readme.md --- .github/workflows/docker_update_readme.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/docker_update_readme.yml diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml new file mode 100644 index 000000000..bc063617a --- /dev/null +++ b/.github/workflows/docker_update_readme.yml @@ -0,0 +1,18 @@ +name: Update Docker Hub Description +on: + push: + branches: + - master + +jobs: + dockerHubDescription: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v2.0.0 + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade + From 136ef077b2c96179981c1980dc7827a555cca981 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 13:14:43 +0100 Subject: [PATCH 187/319] Add sleep to allow thread to start --- tests/rpc/test_rpc_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 7278f0671..c9fbf8c3b 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 - +import time import logging from unittest.mock import MagicMock @@ -176,6 +176,8 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: "listen_port": "8080"} rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + # Sleep to allow the thread to start + time.sleep(0.5) 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] From ab9506df480e53316b65cd43d72c757e3dc18ab4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 13:54:26 +0100 Subject: [PATCH 188/319] simplify status_table command --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 16 +++++++--------- freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc.py | 12 +++++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 5167823fd..3b59c9592 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -274,7 +274,7 @@ class ApiServer(RPC): stats = self._rpc_daily_profit(timescale, self._config['stake_currency'], - self._config['fiat_display_currency'] + self._config.get('fiat_display_currency', '') ) return self.rest_dump(stats) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4aed48f74..84e681992 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -3,16 +3,15 @@ This module contains class to define a RPC communications """ import logging from abc import abstractmethod -from datetime import timedelta, datetime, date +from datetime import date, datetime, timedelta from decimal import Decimal from enum import Enum -from typing import Dict, Any, List, Optional +from typing import Any, Dict, List, Optional, Tuple import arrow -from numpy import mean, NAN -from pandas import DataFrame +from numpy import NAN, mean -from freqtrade import TemporaryError, DependencyException +from freqtrade import DependencyException, TemporaryError from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -117,7 +116,7 @@ class RPC: results.append(trade_dict) return results - def _rpc_status_table(self) -> DataFrame: + def _rpc_status_table(self, fiat_display_currency: str) -> Tuple[List, List]: trades = Trade.get_open_trades() if not trades: raise RPCException('no active order') @@ -135,12 +134,11 @@ class RPC: trade.pair, shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), f'{trade_perc:.2f}%' + ]) columns = ['ID', 'Pair', 'Since', 'Profit'] - df_statuses = DataFrame.from_records(trades_list, columns=columns) - df_statuses = df_statuses.set_index(columns[0]) - return df_statuses + return trades_list, columns def _rpc_daily_profit( self, timescale: int, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 80582a0ce..25422973d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -234,8 +234,8 @@ class Telegram(RPC): :return: None """ try: - df_statuses = self._rpc_status_table() - message = tabulate(df_statuses, headers='keys', tablefmt='simple') + statlist, head = self._rpc_status_table(self._config.get('fiat_display_currency', '')) + message = tabulate(statlist, headers=head, tablefmt='simple') self._send_msg(f"

    {message}
    ", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index df2261c1f..06203a3d7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -109,13 +109,15 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING with pytest.raises(RPCException, match=r'.*no active order*'): - rpc._rpc_status_table() + rpc._rpc_status_table('USD') freqtradebot.create_trades() - result = rpc._rpc_status_table() - assert 'instantly' in result['Since'].all() - assert 'ETH/BTC' in result['Pair'].all() - assert '-0.59%' in result['Profit'].all() + result, headers = rpc._rpc_status_table('USD') + assert "Since" in headers + assert "Pair" in headers + assert 'instantly' in result[0][2] + assert 'ETH/BTC' in result[0][1] + assert '-0.59%' in result[0][3] mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) From df9bfb6c2e02b476955d137d45c637105ae2ed16 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 14:58:41 +0100 Subject: [PATCH 189/319] Add FIAT currency to status-table --- freqtrade/rpc/rpc.py | 21 +++++++++++++++++---- freqtrade/rpc/telegram.py | 3 ++- tests/rpc/test_rpc.py | 33 ++++++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 84e681992..f15bf3cd0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -6,6 +6,7 @@ from abc import abstractmethod from datetime import date, datetime, timedelta from decimal import Decimal from enum import Enum +from math import isnan from typing import Any, Dict, List, Optional, Tuple import arrow @@ -116,7 +117,7 @@ class RPC: results.append(trade_dict) return results - def _rpc_status_table(self, fiat_display_currency: str) -> Tuple[List, List]: + def _rpc_status_table(self, stake_currency, fiat_display_currency: str) -> Tuple[List, List]: trades = Trade.get_open_trades() if not trades: raise RPCException('no active order') @@ -129,15 +130,27 @@ class RPC: except DependencyException: current_rate = NAN trade_perc = (100 * trade.calc_profit_percent(current_rate)) + trade_profit = trade.calc_profit(current_rate) + profit_str = f'{trade_perc:.2f}%' + if self._fiat_converter: + fiat_profit = self._fiat_converter.convert_amount( + trade_profit, + stake_currency, + fiat_display_currency + ) + if fiat_profit and not isnan(fiat_profit): + profit_str += f" ({fiat_profit:.2f})" trades_list.append([ trade.id, trade.pair, shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), - f'{trade_perc:.2f}%' - + profit_str ]) + profitcol = "Profit" + if self._fiat_converter: + profitcol += " (" + fiat_display_currency + ")" - columns = ['ID', 'Pair', 'Since', 'Profit'] + columns = ['ID', 'Pair', 'Since', profitcol] return trades_list, columns def _rpc_daily_profit( diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 25422973d..8a81848ac 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -234,7 +234,8 @@ class Telegram(RPC): :return: None """ try: - statlist, head = self._rpc_status_table(self._config.get('fiat_display_currency', '')) + statlist, head = self._rpc_status_table(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) message = tabulate(statlist, headers=head, tablefmt='simple') self._send_msg(f"
    {message}
    ", parse_mode=ParseMode.HTML) except RPCException as e: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 06203a3d7..e7f41f6e7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -96,6 +96,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.rpc.fiat_convert.Market', + ticker=MagicMock(return_value={'price_usd': 15000.0}), + ) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -109,24 +114,34 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING with pytest.raises(RPCException, match=r'.*no active order*'): - rpc._rpc_status_table('USD') + rpc._rpc_status_table(default_conf['stake_currency'], 'USD') freqtradebot.create_trades() - result, headers = rpc._rpc_status_table('USD') + + result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers assert "Pair" in headers - assert 'instantly' in result[0][2] - assert 'ETH/BTC' in result[0][1] - assert '-0.59%' in result[0][3] + assert 'instantly' == result[0][2] + assert 'ETH/BTC' == result[0][1] + assert '-0.59%' == result[0][3] + # Test with fiatconvert + + rpc._fiat_converter = CryptoToFiatConverter() + result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + assert "Since" in headers + assert "Pair" in headers + assert 'instantly' == result[0][2] + assert 'ETH/BTC' == result[0][1] + assert '-0.59% (-0.09)' == result[0][3] mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} - result = rpc._rpc_status_table() - assert 'instantly' in result['Since'].all() - assert 'ETH/BTC' in result['Pair'].all() - assert 'nan%' in result['Profit'].all() + result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + assert 'instantly' == result[0][2] + assert 'ETH/BTC' == result[0][1] + assert 'nan%' == result[0][3] def test_rpc_daily_profit(default_conf, update, ticker, fee, From 11f7ab61b9cf7958f44f2480c11cc787c1c21fb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 15:11:31 +0100 Subject: [PATCH 190/319] Remove decimal import from rpc --- freqtrade/rpc/rpc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f15bf3cd0..5ab92cf33 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -4,7 +4,6 @@ This module contains class to define a RPC communications import logging from abc import abstractmethod from datetime import date, datetime, timedelta -from decimal import Decimal from enum import Enum from math import isnan from typing import Any, Dict, List, Optional, Tuple @@ -230,7 +229,7 @@ class RPC: profit_percent = trade.calc_profit_percent(rate=current_rate) profit_all_coin.append( - trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)) + trade.calc_profit(rate=trade.close_rate or current_rate) ) profit_all_perc.append(profit_percent) From e4bdb92521d38c062a0b266345727cfbe893899a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 20:19:13 +0100 Subject: [PATCH 191/319] Replace some occurances of ticker_interval with timeframe --- freqtrade/data/converter.py | 10 ++--- freqtrade/data/dataprovider.py | 16 +++---- freqtrade/data/history.py | 72 +++++++++++++++--------------- freqtrade/edge/__init__.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/plot/plotting.py | 13 +++--- tests/data/test_btanalysis.py | 8 ++-- tests/data/test_converter.py | 2 +- tests/data/test_dataprovider.py | 2 +- tests/data/test_history.py | 58 ++++++++++++------------ tests/edge/test_edge.py | 2 +- tests/optimize/test_backtesting.py | 16 +++---- tests/test_plotting.py | 10 ++--- 13 files changed, 107 insertions(+), 106 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 1ef224978..e45dd451e 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,13 +10,13 @@ from pandas import DataFrame, to_datetime logger = logging.getLogger(__name__) -def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *, +def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *, fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame: """ Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe :param ticker: ticker list, as returned by exchange.async_get_candle_history - :param ticker_interval: ticker_interval (e.g. 5m). Used to fill up eventual missing data + :param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data :param pair: Pair this data is for (used to warn if fillup was necessary) :param fill_missing: fill up missing candles with 0 candles (see ohlcv_fill_up_missing_data for details) @@ -52,12 +52,12 @@ def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *, logger.debug('Dropping last candle') if fill_missing: - return ohlcv_fill_up_missing_data(frame, ticker_interval, pair) + return ohlcv_fill_up_missing_data(frame, timeframe, pair) else: return frame -def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str, pair: str) -> DataFrame: +def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame: """ Fills up missing data with 0 volume rows, using the previous close as price for "open", "high" "low" and "close", volume is set to 0 @@ -72,7 +72,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str, pair: 'close': 'last', 'volume': 'sum' } - ticker_minutes = timeframe_to_minutes(ticker_interval) + ticker_minutes = timeframe_to_minutes(timeframe) # Resample to create "NAN" values df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index f0787281a..ce4554cbb 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -42,29 +42,29 @@ class DataProvider: """ return list(self._exchange._klines.keys()) - def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: + def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: """ Get ohlcv data for the given pair as DataFrame 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 timeframe: Ticker timeframe 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): - return self._exchange.klines((pair, ticker_interval or self._config['ticker_interval']), + return self._exchange.klines((pair, timeframe or self._config['ticker_interval']), copy=copy) else: return DataFrame() - def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame: + def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame: """ Get stored historic ohlcv data :param pair: pair to get the data for - :param ticker_interval: ticker interval to get data for + :param timeframe: ticker interval to get data for """ return load_pair_history(pair=pair, - ticker_interval=ticker_interval or self._config['ticker_interval'], + timeframe=timeframe or self._config['ticker_interval'], datadir=Path(self._config['datadir']) ) @@ -77,10 +77,10 @@ class DataProvider: """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): # Get live ohlcv data. - data = self.ohlcv(pair=pair, ticker_interval=ticker_interval) + data = self.ohlcv(pair=pair, timeframe=ticker_interval) else: # Get historic ohlcv data (cached on disk). - data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval) + data = self.historic_ohlcv(pair=pair, timeframe=ticker_interval) if len(data) == 0: logger.warning(f"No data found for ({pair}, {ticker_interval}).") return data diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 3dd40d2b4..8e4bc8ced 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -63,13 +63,13 @@ def trim_dataframe(df: DataFrame, timerange: TimeRange) -> DataFrame: return df -def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str, +def load_tickerdata_file(datadir: Path, pair: str, timeframe: str, timerange: Optional[TimeRange] = None) -> Optional[list]: """ Load a pair from file, either .json.gz or .json :return: tickerlist or None if unsuccessful """ - filename = pair_data_filename(datadir, pair, ticker_interval) + filename = pair_data_filename(datadir, pair, timeframe) pairdata = misc.file_load_json(filename) if not pairdata: return [] @@ -80,11 +80,11 @@ def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str, def store_tickerdata_file(datadir: Path, pair: str, - ticker_interval: str, data: list, is_zip: bool = False): + timeframe: str, data: list, is_zip: bool = False): """ Stores tickerdata to file """ - filename = pair_data_filename(datadir, pair, ticker_interval) + filename = pair_data_filename(datadir, pair, timeframe) misc.file_dump_json(filename, data, is_zip=is_zip) @@ -121,7 +121,7 @@ def _validate_pairdata(pair, pairdata, timerange: TimeRange): def load_pair_history(pair: str, - ticker_interval: str, + timeframe: str, datadir: Path, timerange: Optional[TimeRange] = None, refresh_pairs: bool = False, @@ -133,7 +133,7 @@ def load_pair_history(pair: str, """ Loads cached ticker history for the given pair. :param pair: Pair to load data for - :param ticker_interval: Ticker-interval (e.g. "5m") + :param timeframe: Ticker timeframe (e.g. "5m") :param datadir: Path to the data storage location. :param timerange: Limit data to be loaded to this timerange :param refresh_pairs: Refresh pairs from exchange. @@ -147,34 +147,34 @@ def load_pair_history(pair: str, timerange_startup = deepcopy(timerange) if startup_candles > 0 and timerange_startup: - timerange_startup.subtract_start(timeframe_to_seconds(ticker_interval) * startup_candles) + timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) # The user forced the refresh of pairs if refresh_pairs: download_pair_history(datadir=datadir, exchange=exchange, pair=pair, - ticker_interval=ticker_interval, + timeframe=timeframe, timerange=timerange) - pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange_startup) + pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup) if pairdata: if timerange_startup: _validate_pairdata(pair, pairdata, timerange_startup) - return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair, + return parse_ticker_dataframe(pairdata, timeframe, pair=pair, fill_missing=fill_up_missing, drop_incomplete=drop_incomplete) else: logger.warning( - f'No history data for pair: "{pair}", interval: {ticker_interval}. ' + f'No history data for pair: "{pair}", timeframe: {timeframe}. ' 'Use `freqtrade download-data` to download the data' ) return None def load_data(datadir: Path, - ticker_interval: str, + timeframe: str, pairs: List[str], refresh_pairs: bool = False, exchange: Optional[Exchange] = None, @@ -186,7 +186,7 @@ def load_data(datadir: Path, """ Loads ticker history data for a list of pairs :param datadir: Path to the data storage location. - :param ticker_interval: Ticker-interval (e.g. "5m") + :param timeframe: Ticker Timeframe (e.g. "5m") :param pairs: List of pairs to load :param refresh_pairs: Refresh pairs from exchange. (Note: Requires exchange to be passed as well.) @@ -206,7 +206,7 @@ def load_data(datadir: Path, logger.info(f'Using indicator startup period: {startup_candles} ...') for pair in pairs: - hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, + hist = load_pair_history(pair=pair, timeframe=timeframe, datadir=datadir, timerange=timerange, refresh_pairs=refresh_pairs, exchange=exchange, @@ -220,9 +220,9 @@ def load_data(datadir: Path, return result -def pair_data_filename(datadir: Path, pair: str, ticker_interval: str) -> Path: +def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path: pair_s = pair.replace("/", "_") - filename = datadir.joinpath(f'{pair_s}-{ticker_interval}.json') + filename = datadir.joinpath(f'{pair_s}-{timeframe}.json') return filename @@ -232,7 +232,7 @@ def pair_trades_filename(datadir: Path, pair: str) -> Path: return filename -def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str, +def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ @@ -250,12 +250,12 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: st if timerange.starttype == 'date': since_ms = timerange.startts * 1000 elif timerange.stoptype == 'line': - num_minutes = timerange.stopts * timeframe_to_minutes(ticker_interval) + num_minutes = timerange.stopts * timeframe_to_minutes(timeframe) since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file # Intentionally don't pass timerange in - since we need to load the full dataset. - data = load_tickerdata_file(datadir, pair, ticker_interval) + data = load_tickerdata_file(datadir, pair, timeframe) # remove the last item, could be incomplete candle if data: data.pop() @@ -276,18 +276,18 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: st def download_pair_history(datadir: Path, exchange: Optional[Exchange], pair: str, - ticker_interval: str = '5m', + timeframe: str = '5m', timerange: Optional[TimeRange] = None) -> bool: """ Download the latest ticker intervals from the exchange for the pair passed in parameters - The data is downloaded starting from the last correct ticker interval data that + The data is downloaded starting from the last correct data that exists in a cache. If timerange starts earlier than the data in the cache, the full data will be redownloaded Based on @Rybolov work: https://github.com/rybolov/freqtrade-data :param pair: pair to download - :param ticker_interval: ticker interval + :param timeframe: Ticker Timeframe (e.g 5m) :param timerange: range of time to download :return: bool with success state """ @@ -298,17 +298,17 @@ def download_pair_history(datadir: Path, try: logger.info( - f'Download history data for pair: "{pair}", interval: {ticker_interval} ' + f'Download history data for pair: "{pair}", timeframe: {timeframe} ' f'and store in {datadir}.' ) - data, since_ms = _load_cached_data_for_updating(datadir, pair, ticker_interval, timerange) + data, since_ms = _load_cached_data_for_updating(datadir, pair, timeframe, 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') # Default since_ms to 30 days if nothing is given - new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, + new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=timeframe, since_ms=since_ms if since_ms else int(arrow.utcnow().shift( @@ -318,12 +318,12 @@ def download_pair_history(datadir: 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])) - store_tickerdata_file(datadir, pair, ticker_interval, data=data) + store_tickerdata_file(datadir, pair, timeframe, data=data) return True except Exception as e: logger.error( - f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}. ' + f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}. ' f'Error: {e}' ) return False @@ -343,17 +343,17 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes pairs_not_available.append(pair) logger.info(f"Skipping pair {pair}...") continue - for ticker_interval in timeframes: + for timeframe in timeframes: - dl_file = pair_data_filename(dl_path, pair, ticker_interval) + dl_file = pair_data_filename(dl_path, pair, timeframe) if erase and dl_file.exists(): logger.info( - f'Deleting existing data for pair {pair}, interval {ticker_interval}.') + f'Deleting existing data for pair {pair}, interval {timeframe}.') dl_file.unlink() - logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') + logger.info(f'Downloading pair {pair}, interval {timeframe}.') download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, ticker_interval=str(ticker_interval), + pair=pair, timeframe=str(timeframe), timerange=timerange) return pairs_not_available @@ -459,7 +459,7 @@ def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, - max_date: datetime, ticker_interval_mins: int) -> bool: + max_date: datetime, timeframe_mins: int) -> bool: """ Validates preprocessed backtesting data for missing values and shows warnings about it that. @@ -467,10 +467,10 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, :param pair: pair used for log output. :param min_date: start-date of the data :param max_date: end-date of the data - :param ticker_interval_mins: ticker interval in minutes + :param timeframe_mins: ticker Timeframe in minutes """ - # total difference in minutes / interval-minutes - expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) + # total difference in minutes / timeframe-minutes + expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_mins) found_missing = False dflen = len(data) if dflen < expected_frames: diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 883bf4a0f..afd20cf61 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -97,7 +97,7 @@ class Edge: data = history.load_data( datadir=Path(self.config['datadir']), pairs=pairs, - ticker_interval=self.strategy.ticker_interval, + timeframe=self.strategy.ticker_interval, refresh_pairs=self._refresh_pairs, exchange=self.exchange, timerange=self._timerange, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ee3a135d2..58fd1f772 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -108,7 +108,7 @@ class Backtesting: data = history.load_data( datadir=Path(self.config['datadir']), pairs=self.config['exchange']['pair_whitelist'], - ticker_interval=self.ticker_interval, + timeframe=self.ticker_interval, timerange=timerange, startup_candles=self.required_startup, fail_without_data=True, diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index bbdb52ca1..01396aea9 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -39,7 +39,7 @@ def init_plotscript(config): tickers = history.load_data( datadir=Path(str(config.get("datadir"))), pairs=pairs, - ticker_interval=config.get('ticker_interval', '5m'), + timeframe=config.get('ticker_interval', '5m'), timerange=timerange, ) @@ -300,12 +300,12 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], return fig -def generate_plot_filename(pair, ticker_interval) -> str: +def generate_plot_filename(pair, timeframe) -> str: """ - Generate filenames per pair/ticker_interval to be used for storing plots + Generate filenames per pair/timeframe to be used for storing plots """ pair_name = pair.replace("/", "_") - file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' + file_name = 'freqtrade-plot-' + pair_name + '-' + timeframe + '.html' logger.info('Generate plot file for %s', pair) @@ -316,8 +316,9 @@ def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False """ Generate a plot html file from pre populated fig plotly object :param fig: Plotly Figure to plot - :param pair: Pair to plot (used as filename and Plot title) - :param ticker_interval: Used as part of the filename + :param filename: Name to store the file as + :param directory: Directory to store the file in + :param auto_open: Automatically open files saved :return: None """ directory.mkdir(parents=True, exist_ok=True) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index b49344bbd..13711c63e 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -56,7 +56,7 @@ def test_extract_trades_of_period(testdatadir): # 2018-11-14 06:07:00 timerange = TimeRange('date', None, 1510639620, 0) - data = load_pair_history(pair=pair, ticker_interval='1m', + data = load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) trades = DataFrame( @@ -122,7 +122,7 @@ def test_combine_tickers_with_mean(testdatadir): pairs = ["ETH/BTC", "ADA/BTC"] tickers = load_data(datadir=testdatadir, pairs=pairs, - ticker_interval='5m' + timeframe='5m' ) df = combine_tickers_with_mean(tickers) assert isinstance(df, DataFrame) @@ -136,7 +136,7 @@ def test_create_cum_profit(testdatadir): bt_data = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - df = load_pair_history(pair="TRX/BTC", ticker_interval='5m', + df = load_pair_history(pair="TRX/BTC", timeframe='5m', datadir=testdatadir, timerange=timerange) cum_profits = create_cum_profit(df.set_index('date'), @@ -154,7 +154,7 @@ def test_create_cum_profit1(testdatadir): bt_data.loc[:, 'close_time'] = bt_data.loc[:, 'close_time'] + DateOffset(seconds=20) timerange = TimeRange.parse_timerange("20180110-20180112") - df = load_pair_history(pair="TRX/BTC", ticker_interval='5m', + df = load_pair_history(pair="TRX/BTC", timeframe='5m', datadir=testdatadir, timerange=timerange) cum_profits = create_cum_profit(df.set_index('date'), diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index e773a970e..92494ff1e 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -23,7 +23,7 @@ def test_parse_ticker_dataframe(ticker_history_list, caplog): def test_ohlcv_fill_up_missing_data(testdatadir, caplog): data = load_pair_history(datadir=testdatadir, - ticker_interval='1m', + timeframe='1m', pair='UNITTEST/BTC', fill_up_missing=False) caplog.set_level(logging.DEBUG) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 9a857750b..0318e5a82 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -45,7 +45,7 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history): data = dp.historic_ohlcv("UNITTEST/BTC", "5m") assert isinstance(data, DataFrame) assert historymock.call_count == 1 - assert historymock.call_args_list[0][1]["ticker_interval"] == "5m" + assert historymock.call_args_list[0][1]["timeframe"] == "5m" def test_get_pair_dataframe(mocker, default_conf, ticker_history): diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 89120b4f5..65feaf03e 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -64,20 +64,20 @@ def _clean_test_file(file: Path) -> None: def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None: - ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='30m', datadir=testdatadir) + ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert not log_has( - 'Download history data for pair: "UNITTEST/BTC", interval: 30m ' + 'Download history data for pair: "UNITTEST/BTC", timeframe: 30m ' 'and store in None.', caplog ) def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None: - ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='7m', datadir=testdatadir) + ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) assert not isinstance(ld, DataFrame) assert ld is None assert log_has( - 'No history data for pair: "UNITTEST/BTC", interval: 7m. ' + 'No history data for pair: "UNITTEST/BTC", timeframe: 7m. ' 'Use `freqtrade download-data` to download the data', caplog ) @@ -86,7 +86,7 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> N mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) file = testdatadir / 'UNITTEST_BTC-1m.json' _backup_file(file, copy_file=True) - history.load_data(datadir=testdatadir, ticker_interval='1m', pairs=['UNITTEST/BTC']) + history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) assert file.is_file() assert not log_has( 'Download history data for pair: "UNITTEST/BTC", interval: 1m ' @@ -99,7 +99,7 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> ltfmock = mocker.patch('freqtrade.data.history.load_tickerdata_file', MagicMock(return_value=None)) timerange = TimeRange('date', None, 1510639620, 0) - history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='1m', + history.load_pair_history(pair='UNITTEST/BTC', timeframe='1m', datadir=testdatadir, timerange=timerange, startup_candles=20, ) @@ -122,28 +122,28 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, _backup_file(file) # do not download a new pair if refresh_pairs isn't set history.load_pair_history(datadir=testdatadir, - ticker_interval='1m', + timeframe='1m', pair='MEME/BTC') assert not file.is_file() assert log_has( - 'No history data for pair: "MEME/BTC", interval: 1m. ' + 'No history data for pair: "MEME/BTC", timeframe: 1m. ' 'Use `freqtrade download-data` to download the data', caplog ) # download a new pair if refresh_pairs is set history.load_pair_history(datadir=testdatadir, - ticker_interval='1m', + timeframe='1m', refresh_pairs=True, exchange=exchange, pair='MEME/BTC') assert file.is_file() assert log_has_re( - 'Download history data for pair: "MEME/BTC", interval: 1m ' + 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): history.load_pair_history(datadir=testdatadir, - ticker_interval='1m', + timeframe='1m', refresh_pairs=True, exchange=None, pair='MEME/BTC') @@ -269,10 +269,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda assert download_pair_history(datadir=testdatadir, exchange=exchange, pair='MEME/BTC', - ticker_interval='1m') + timeframe='1m') assert download_pair_history(datadir=testdatadir, exchange=exchange, pair='CFI/BTC', - ticker_interval='1m') + timeframe='1m') assert not exchange._pairs_last_refresh_time assert file1_1.is_file() assert file2_1.is_file() @@ -286,10 +286,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda assert download_pair_history(datadir=testdatadir, exchange=exchange, pair='MEME/BTC', - ticker_interval='5m') + timeframe='5m') assert download_pair_history(datadir=testdatadir, exchange=exchange, pair='CFI/BTC', - ticker_interval='5m') + timeframe='5m') assert not exchange._pairs_last_refresh_time assert file1_5.is_file() assert file2_5.is_file() @@ -307,8 +307,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) - download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", ticker_interval='1m') - download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", ticker_interval='3m') + download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') + download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') assert json_dump_mock.call_count == 2 @@ -326,12 +326,12 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, assert not download_pair_history(datadir=testdatadir, exchange=exchange, pair='MEME/BTC', - ticker_interval='1m') + timeframe='1m') # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) assert log_has( - 'Failed to download history data for pair: "MEME/BTC", interval: 1m. ' + 'Failed to download history data for pair: "MEME/BTC", timeframe: 1m. ' 'Error: File Error', caplog ) @@ -369,7 +369,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: caplog.clear() start = arrow.get('2018-01-10T00:00:00') end = arrow.get('2018-02-20T00:00:00') - tickerdata = history.load_data(datadir=testdatadir, ticker_interval='5m', + tickerdata = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) @@ -390,7 +390,7 @@ def test_init(default_conf, mocker) -> None: exchange=exchange, pairs=[], refresh_pairs=True, - ticker_interval=default_conf['ticker_interval'] + timeframe=default_conf['ticker_interval'] ) @@ -449,7 +449,7 @@ def test_trim_tickerlist(testdatadir) -> None: def test_trim_dataframe(testdatadir) -> None: data = history.load_data( datadir=testdatadir, - ticker_interval='1m', + timeframe='1m', pairs=['UNITTEST/BTC'] )['UNITTEST/BTC'] min_date = int(data.iloc[0]['date'].timestamp()) @@ -517,7 +517,7 @@ def test_get_timeframe(default_conf, mocker, testdatadir) -> None: data = strategy.tickerdata_to_dataframe( history.load_data( datadir=testdatadir, - ticker_interval='1m', + timeframe='1m', pairs=['UNITTEST/BTC'] ) ) @@ -533,7 +533,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) data = strategy.tickerdata_to_dataframe( history.load_data( datadir=testdatadir, - ticker_interval='1m', + timeframe='1m', pairs=['UNITTEST/BTC'], fill_up_missing=False ) @@ -556,7 +556,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No data = strategy.tickerdata_to_dataframe( history.load_data( datadir=testdatadir, - ticker_interval='5m', + timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange ) @@ -669,10 +669,10 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): file5 = testdatadir / 'XRP_ETH-5m.json' # Compare downloaded dataset with converted dataset dfbak_1m = history.load_pair_history(datadir=testdatadir, - ticker_interval="1m", + timeframe="1m", pair=pair) dfbak_5m = history.load_pair_history(datadir=testdatadir, - ticker_interval="5m", + timeframe="5m", pair=pair) _backup_file(file1, copy_file=True) @@ -686,10 +686,10 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) # Load new data df_1m = history.load_pair_history(datadir=testdatadir, - ticker_interval="1m", + timeframe="1m", pair=pair) df_5m = history.load_pair_history(datadir=testdatadir, - ticker_interval="5m", + timeframe="5m", pair=pair) assert df_1m.equals(dfbak_1m) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index e1af50768..001dc9591 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -255,7 +255,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): assert edge.calculate() is False -def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, +def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, timerange=None, exchange=None, *args, **kwargs): hz = 0.1 base = 0.001 diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5912c5489..a5ab6d84c 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -50,7 +50,7 @@ def trim_dictlist(dict_list, num): def load_data_test(what, testdatadir): timerange = TimeRange.parse_timerange('1510694220-1510700340') - pair = history.load_tickerdata_file(testdatadir, ticker_interval='1m', + pair = history.load_tickerdata_file(testdatadir, timeframe='1m', pair='UNITTEST/BTC', timerange=timerange) datalen = len(pair) @@ -116,7 +116,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: assert len(results) == num_results -def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, +def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, timerange=None, exchange=None, live=False, *args, **kwargs): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC", @@ -126,14 +126,14 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals # use for mock ccxt.fetch_ohlvc' def _load_pair_as_ticks(pair, tickfreq): - ticks = history.load_tickerdata_file(None, ticker_interval=tickfreq, pair=pair) + ticks = history.load_tickerdata_file(None, timeframe=tickfreq, pair=pair) ticks = ticks[-201:] return ticks # FIX: fixturize this? def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record=None): - data = history.load_data(datadir=datadir, ticker_interval='1m', pairs=[pair]) + data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair]) data = trim_dictlist(data, -201) patch_exchange(mocker) backtesting = Backtesting(conf) @@ -522,7 +522,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' timerange = TimeRange('date', None, 1517227800, 0) - data = history.load_data(datadir=testdatadir, ticker_interval='5m', pairs=['UNITTEST/BTC'], + data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timeframe(data_processed) @@ -576,9 +576,9 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) - patch_exchange(mocker) backtesting = Backtesting(default_conf) - # Run a backtesting for an exiting 1min ticker_interval + # Run a backtesting for an exiting 1min timeframe timerange = TimeRange.parse_timerange('1510688220-1510700340') - data = history.load_data(datadir=testdatadir, ticker_interval='1m', pairs=['UNITTEST/BTC'], + data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], timerange=timerange) processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timeframe(processed) @@ -688,7 +688,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) patch_exchange(mocker) pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] - data = history.load_data(datadir=testdatadir, ticker_interval='5m', pairs=pairs) + data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=pairs) # Only use 500 lines to increase performance data = trim_dictlist(data, -500) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 4a6efcd8e..f0d9578ac 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -64,7 +64,7 @@ def test_add_indicators(default_conf, testdatadir, caplog): pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) - data = history.load_pair_history(pair=pair, ticker_interval='1m', + data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) indicators1 = ["ema10"] indicators2 = ["macd"] @@ -129,7 +129,7 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) - data = history.load_pair_history(pair=pair, ticker_interval='1m', + data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) data['buy'] = 0 data['sell'] = 0 @@ -164,7 +164,7 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir) MagicMock(side_effect=fig_generating_mock)) pair = 'UNITTEST/BTC' timerange = TimeRange(None, 'line', 0, -1000) - data = history.load_pair_history(pair=pair, ticker_interval='1m', + data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) # Generate buy/sell signals and indicators @@ -228,7 +228,7 @@ def test_add_profit(testdatadir): bt_data = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - df = history.load_pair_history(pair="TRX/BTC", ticker_interval='5m', + df = history.load_pair_history(pair="TRX/BTC", timeframe='5m', datadir=testdatadir, timerange=timerange) fig = generate_empty_figure() @@ -251,7 +251,7 @@ def test_generate_profit_graph(testdatadir): tickers = history.load_data(datadir=testdatadir, pairs=pairs, - ticker_interval='5m', + timeframe='5m', timerange=timerange ) trades = trades[trades['pair'].isin(pairs)] From 08aedc18e1eb2bf7ff0ead368ec043c5585c17f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 20:25:18 +0100 Subject: [PATCH 192/319] Exchange ticker_interval with timeframe in some more places --- freqtrade/data/dataprovider.py | 13 ++++--- freqtrade/data/history.py | 2 +- freqtrade/exchange/exchange.py | 67 ++++++++++++++++----------------- tests/exchange/test_exchange.py | 4 +- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index ce4554cbb..db71ff029 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -37,7 +37,7 @@ class DataProvider: @property def available_pairs(self) -> List[Tuple[str, str]]: """ - Return a list of tuples containing pair, ticker_interval for which data is currently cached. + Return a list of tuples containing (pair, timeframe) for which data is currently cached. Should be whitelist + open trades. """ return list(self._exchange._klines.keys()) @@ -68,21 +68,22 @@ class DataProvider: datadir=Path(self._config['datadir']) ) - def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame: + def get_pair_dataframe(self, pair: str, timeframe: 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 + :param timeframe: ticker interval to get data for + :return: Dataframe for this pair """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): # Get live ohlcv data. - data = self.ohlcv(pair=pair, timeframe=ticker_interval) + data = self.ohlcv(pair=pair, timeframe=timeframe) else: # Get historic ohlcv data (cached on disk). - data = self.historic_ohlcv(pair=pair, timeframe=ticker_interval) + data = self.historic_ohlcv(pair=pair, timeframe=timeframe) if len(data) == 0: - logger.warning(f"No data found for ({pair}, {ticker_interval}).") + logger.warning(f"No data found for ({pair}, {timeframe}).") return data def market(self, pair: str) -> Optional[Dict[str, Any]]: diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 8e4bc8ced..3dea41c55 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -308,7 +308,7 @@ def download_pair_history(datadir: 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_historic_ohlcv(pair=pair, ticker_interval=timeframe, + new_data = exchange.get_historic_ohlcv(pair=pair, timeframe=timeframe, since_ms=since_ms if since_ms else int(arrow.utcnow().shift( diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a198e8cdb..05db45c9b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -536,40 +536,40 @@ class Exchange: logger.info("returning cached ticker-data for %s", pair) return self._cached_ticker[pair] - def get_historic_ohlcv(self, pair: str, ticker_interval: str, + def get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: """ 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 timeframe: Ticker Timeframe 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_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, + self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, since_ms=since_ms)) async def _async_get_historic_ohlcv(self, pair: str, - ticker_interval: str, + timeframe: str, since_ms: int) -> List: - one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit + one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit logger.debug( "one_call: %s msecs (%s)", one_call, arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True) ) input_coroutines = [self._async_get_candle_history( - pair, ticker_interval, since) for since in + pair, timeframe, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) # Combine tickers data: List = [] - for p, ticker_interval, ticker in tickers: + for p, timeframe, ticker in tickers: if p == pair: data.extend(ticker) # Sort data again after extending the result - above calls return in "async order" @@ -589,14 +589,14 @@ class Exchange: input_coroutines = [] # Gather coroutines to run - for pair, ticker_interval in set(pair_list): - if (not ((pair, ticker_interval) in self._klines) - or self._now_is_time_to_refresh(pair, ticker_interval)): - input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) + for pair, timeframe in set(pair_list): + if (not ((pair, timeframe) in self._klines) + or self._now_is_time_to_refresh(pair, timeframe)): + input_coroutines.append(self._async_get_candle_history(pair, timeframe)) else: logger.debug( - "Using cached ohlcv data for pair %s, interval %s ...", - pair, ticker_interval + "Using cached ohlcv data for pair %s, timeframe %s ...", + pair, timeframe ) tickers = asyncio.get_event_loop().run_until_complete( @@ -608,40 +608,40 @@ class Exchange: logger.warning("Async code raised an exception: %s", res.__class__.__name__) continue pair = res[0] - ticker_interval = res[1] + timeframe = res[1] ticks = res[2] # keeping last candle time as last refreshed time of the pair if ticks: - self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 + self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache - self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( - ticks, ticker_interval, pair=pair, fill_missing=True, + self._klines[(pair, timeframe)] = parse_ticker_dataframe( + ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=self._ohlcv_partial_candle) return tickers - def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: + def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool: # Calculating ticker interval in seconds - interval_in_sec = timeframe_to_seconds(ticker_interval) + interval_in_sec = timeframe_to_seconds(timeframe) - return not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0) + return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0) + interval_in_sec) >= arrow.utcnow().timestamp) @retrier_async - async def _async_get_candle_history(self, pair: str, ticker_interval: str, + async def _async_get_candle_history(self, pair: str, timeframe: str, since_ms: Optional[int] = None) -> Tuple[str, str, List]: """ Asynchronously gets candle histories using fetch_ohlcv - returns tuple: (pair, ticker_interval, ohlcv_list) + returns tuple: (pair, timeframe, ohlcv_list) """ try: # fetch ohlcv asynchronously s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' logger.debug( "Fetching pair %s, interval %s, since %s %s...", - pair, ticker_interval, since_ms, s + pair, timeframe, since_ms, s ) - data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, + data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms) # Because some exchange sort Tickers ASC and other DESC. @@ -653,9 +653,9 @@ class Exchange: data = sorted(data, key=lambda x: x[0]) except IndexError: logger.exception("Error loading %s. Result was %s.", pair, data) - return pair, ticker_interval, [] - logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval) - return pair, ticker_interval, data + return pair, timeframe, [] + logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe) + return pair, timeframe, data except ccxt.NotSupported as e: raise OperationalException( @@ -802,7 +802,6 @@ class Exchange: 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: Timestamp in milliseconds to get history from :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined. :param from_id: Download data starting with ID (if id is known) @@ -958,27 +957,27 @@ def available_exchanges(ccxt_module=None) -> List[str]: return [x for x in exchanges if not is_exchange_bad(x)] -def timeframe_to_seconds(ticker_interval: str) -> int: +def timeframe_to_seconds(timeframe: str) -> int: """ Translates the timeframe interval value written in the human readable form ('1m', '5m', '1h', '1d', '1w', etc.) to the number of seconds for one timeframe interval. """ - return ccxt.Exchange.parse_timeframe(ticker_interval) + return ccxt.Exchange.parse_timeframe(timeframe) -def timeframe_to_minutes(ticker_interval: str) -> int: +def timeframe_to_minutes(timeframe: str) -> int: """ Same as timeframe_to_seconds, but returns minutes. """ - return ccxt.Exchange.parse_timeframe(ticker_interval) // 60 + return ccxt.Exchange.parse_timeframe(timeframe) // 60 -def timeframe_to_msecs(ticker_interval: str) -> int: +def timeframe_to_msecs(timeframe: str) -> int: """ Same as timeframe_to_seconds, but returns milliseconds. """ - return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 + return ccxt.Exchange.parse_timeframe(timeframe) * 1000 def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 925a53c95..68fac8632 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1107,7 +1107,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) 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]} ...", + assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, timeframe {pairs[0][1]} ...", caplog) @@ -1143,7 +1143,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_get_candle_history", "fetch_ohlcv", - pair='ABCD/BTC', ticker_interval=default_conf['ticker_interval']) + pair='ABCD/BTC', timeframe=default_conf['ticker_interval']) api_mock = MagicMock() with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): From d801dec6aa45fb1eb8271ad8166e9b44a9608e53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 20:26:26 +0100 Subject: [PATCH 193/319] Some more places with ticker_interval gone --- freqtrade/optimize/backtesting.py | 10 +++++----- tests/data/test_converter.py | 12 ++++++------ tests/data/test_dataprovider.py | 24 ++++++++++++------------ tests/exchange/test_exchange.py | 4 ++-- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 58fd1f772..79478076b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -83,8 +83,8 @@ class Backtesting: 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 = str(self.config.get('ticker_interval')) - self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) + self.timeframe = str(self.config.get('ticker_interval')) + self.timeframe_mins = timeframe_to_minutes(self.timeframe) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -108,7 +108,7 @@ class Backtesting: data = history.load_data( datadir=Path(self.config['datadir']), pairs=self.config['exchange']['pair_whitelist'], - timeframe=self.ticker_interval, + timeframe=self.timeframe, timerange=timerange, startup_candles=self.required_startup, fail_without_data=True, @@ -375,7 +375,7 @@ class Backtesting: lock_pair_until: Dict = {} # Indexes per pair, so some pairs are allowed to have a missing start. indexes: Dict = {} - tmp = start_date + timedelta(minutes=self.ticker_interval_mins) + tmp = start_date + timedelta(minutes=self.timeframe_mins) # Loop timerange and get candle for each pair at that point in time while tmp < end_date: @@ -427,7 +427,7 @@ class Backtesting: lock_pair_until[pair] = end_date.datetime # Move time one configured time_interval ahead. - tmp += timedelta(minutes=self.ticker_interval_mins) + tmp += timedelta(minutes=self.timeframe_mins) return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 92494ff1e..8184167b3 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -42,7 +42,7 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog): def test_ohlcv_fill_up_missing_data2(caplog): - ticker_interval = '5m' + timeframe = '5m' ticks = [[ 1511686200000, # 8:50:00 8.794e-05, # open @@ -78,10 +78,10 @@ def test_ohlcv_fill_up_missing_data2(caplog): ] # Generate test-data without filling missing - data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", fill_missing=False) + data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", fill_missing=False) assert len(data) == 3 caplog.set_level(logging.DEBUG) - data2 = ohlcv_fill_up_missing_data(data, ticker_interval, "UNITTEST/BTC") + data2 = ohlcv_fill_up_missing_data(data, timeframe, "UNITTEST/BTC") assert len(data2) == 4 # 3rd candle has been filled row = data2.loc[2, :] @@ -99,7 +99,7 @@ def test_ohlcv_fill_up_missing_data2(caplog): def test_ohlcv_drop_incomplete(caplog): - ticker_interval = '1d' + timeframe = '1d' ticks = [[ 1559750400000, # 2019-06-04 8.794e-05, # open @@ -134,13 +134,13 @@ def test_ohlcv_drop_incomplete(caplog): ] ] caplog.set_level(logging.DEBUG) - data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", + data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", fill_missing=False, drop_incomplete=False) assert len(data) == 4 assert not log_has("Dropping last candle", caplog) # Drop last candle - data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", + data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", fill_missing=False, drop_incomplete=True) assert len(data) == 3 diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 0318e5a82..1dbe20936 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -9,32 +9,32 @@ from tests.conftest import get_patched_exchange def test_ohlcv(mocker, default_conf, ticker_history): default_conf["runmode"] = RunMode.DRY_RUN - ticker_interval = default_conf["ticker_interval"] + timeframe = 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 + exchange._klines[("XRP/BTC", timeframe)] = ticker_history + exchange._klines[("UNITTEST/BTC", timeframe)] = ticker_history dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN - assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) - assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) - assert dp.ohlcv("UNITTEST/BTC", ticker_interval) is not ticker_history - assert dp.ohlcv("UNITTEST/BTC", ticker_interval, copy=False) is ticker_history - assert not dp.ohlcv("UNITTEST/BTC", ticker_interval).empty - assert dp.ohlcv("NONESENSE/AAA", ticker_interval).empty + assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe)) + assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame) + assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ticker_history + assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ticker_history + assert not dp.ohlcv("UNITTEST/BTC", timeframe).empty + assert dp.ohlcv("NONESENSE/AAA", timeframe).empty # Test with and without parameter - assert dp.ohlcv("UNITTEST/BTC", ticker_interval).equals(dp.ohlcv("UNITTEST/BTC")) + assert dp.ohlcv("UNITTEST/BTC", timeframe).equals(dp.ohlcv("UNITTEST/BTC")) default_conf["runmode"] = RunMode.LIVE dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.LIVE - assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) + assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame) default_conf["runmode"] = RunMode.BACKTEST dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.BACKTEST - assert dp.ohlcv("UNITTEST/BTC", ticker_interval).empty + assert dp.ohlcv("UNITTEST/BTC", timeframe).empty def test_historic_ohlcv(mocker, default_conf, ticker_history): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 68fac8632..a21a5f3ac 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1047,8 +1047,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): ] pair = 'ETH/BTC' - async def mock_candle_hist(pair, ticker_interval, since_ms): - return pair, ticker_interval, tick + async def mock_candle_hist(pair, timeframe, since_ms): + return pair, timeframe, tick exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls From 334ac8b10ccbedd2910cf054b26c18f30e668d96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 20:34:06 +0100 Subject: [PATCH 194/319] Adapt documentation for timeframe --- docs/strategy-customization.md | 8 ++++---- docs/strategy_analysis_example.md | 4 ++-- user_data/notebooks/strategy_analysis_example.ipynb | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 72938f9af..34f86f2ce 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -314,9 +314,9 @@ 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 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). +- `ohlcv(pair, timeframe)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame. +- `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk. +- `get_pair_dataframe(pair, timeframe)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries. - `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure. - `runmode` - Property containing the current runmode. @@ -327,7 +327,7 @@ Please always check the mode of operation to select the correct method to get da if self.dp: inf_pair, inf_timeframe = self.informative_pairs()[0] informative = self.dp.get_pair_dataframe(pair=inf_pair, - ticker_interval=inf_timeframe) + timeframe=inf_timeframe) ``` !!! Warning "Warning about backtesting" diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index aa4578ca7..9e61bda65 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -10,7 +10,7 @@ from pathlib import Path # Customize these according to your needs. # Define some constants -ticker_interval = "5m" +timeframe = "5m" # Name of the strategy class strategy_name = 'SampleStrategy' # Path to user data @@ -29,7 +29,7 @@ pair = "BTC_USDT" from freqtrade.data.history import load_pair_history candles = load_pair_history(datadir=data_location, - ticker_interval=ticker_interval, + timeframe=timeframe, pair=pair) # Confirm success diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/user_data/notebooks/strategy_analysis_example.ipynb index 03dc83b4e..2876ea938 100644 --- a/user_data/notebooks/strategy_analysis_example.ipynb +++ b/user_data/notebooks/strategy_analysis_example.ipynb @@ -26,7 +26,7 @@ "# Customize these according to your needs.\n", "\n", "# Define some constants\n", - "ticker_interval = \"5m\"\n", + "timeframe = \"5m\"\n", "# Name of the strategy class\n", "strategy_name = 'SampleStrategy'\n", "# Path to user data\n", @@ -49,7 +49,7 @@ "from freqtrade.data.history import load_pair_history\n", "\n", "candles = load_pair_history(datadir=data_location,\n", - " ticker_interval=ticker_interval,\n", + " timeframe=timeframe,\n", " pair=pair)\n", "\n", "# Confirm success\n", From 1c57a4ac35435914b1a7330129185ee6dec55be1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 20:34:39 +0100 Subject: [PATCH 195/319] more replacements of ticker_interval --- freqtrade/constants.py | 4 ++-- freqtrade/data/btanalysis.py | 6 +++--- freqtrade/optimize/backtesting.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 5fdd45916..f34232bb1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -24,7 +24,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons -TICKER_INTERVALS = [ +TIMEFRAMES = [ '1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', @@ -57,7 +57,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': 'integer', 'minimum': -1}, - 'ticker_interval': {'type': 'string', 'enum': TICKER_INTERVALS}, + 'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_amount': { "type": ["number", "string"], diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 2f7a234ce..379c80060 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -178,9 +178,9 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, :return: Returns df with one additional column, col_name, containing the cumulative profit. """ from freqtrade.exchange import timeframe_to_minutes - ticker_minutes = timeframe_to_minutes(timeframe) - # Resample to ticker_interval to make sure trades match candles - _trades_sum = trades.resample(f'{ticker_minutes}min', on='close_time')[['profitperc']].sum() + timeframe_minutes = timeframe_to_minutes(timeframe) + # Resample to timeframe to make sure trades match candles + _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time')[['profitperc']].sum() df.loc[:, col_name] = _trades_sum.cumsum() # Set first value to 0 df.loc[df.iloc[0].name, col_name] = 0 diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 79478076b..2c2d116a4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -121,7 +121,7 @@ class Backtesting: min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) # Adjust startts forward if not enough data is available - timerange.adjust_start_if_necessary(timeframe_to_seconds(self.ticker_interval), + timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), self.required_startup, min_date) return data, timerange From c449e3928057213d14d2de2ab6c9f46ac51da5d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Nov 2019 10:01:05 +0100 Subject: [PATCH 196/319] Replace more occurances of ticker_interval --- freqtrade/configuration/timerange.py | 6 +++--- freqtrade/optimize/hyperopt_interface.py | 8 ++++---- tests/optimize/__init__.py | 4 ++-- tests/optimize/test_backtest_detail.py | 4 ++-- tests/optimize/test_backtesting.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 156f0e1e2..a8be873df 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -39,12 +39,12 @@ class TimeRange: if self.startts: self.startts = self.startts - seconds - def adjust_start_if_necessary(self, ticker_interval_secs: int, startup_candles: int, + def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int, min_date: arrow.Arrow) -> None: """ Adjust startts by candles. Applies only if no startup-candles have been available. - :param ticker_interval_secs: Ticker interval in seconds e.g. `timeframe_to_seconds('5m')` + :param timeframe_secs: Ticker timeframe in seconds e.g. `timeframe_to_seconds('5m')` :param startup_candles: Number of candles to move start-date forward :param min_date: Minimum data date loaded. Key kriterium to decide if start-time has to be moved @@ -55,7 +55,7 @@ class TimeRange: # If no startts was defined, or backtest-data starts at the defined backtest-date logger.warning("Moving start-date by %s candles to account for startup time.", startup_candles) - self.startts = (min_date.timestamp + ticker_interval_secs * startup_candles) + self.startts = (min_date.timestamp + timeframe_secs * startup_candles) self.starttype = 'date' @staticmethod diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 142f305df..ac41ba92f 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -106,10 +106,10 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - ticker_interval_mins = timeframe_to_minutes(IHyperOpt.ticker_interval) + timeframe_mins = timeframe_to_minutes(IHyperOpt.ticker_interval) # We define here limits for the ROI space parameters automagically adapted to the - # ticker_interval used by the bot: + # timeframe used by the bot: # # * 'roi_t' (limits for the time intervals in the ROI tables) components # are scaled linearly. @@ -117,8 +117,8 @@ class IHyperOpt(ABC): # # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() # method for the 5m ticker interval. - roi_t_scale = ticker_interval_mins / 5 - roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5) + roi_t_scale = timeframe_mins / 5 + roi_p_scale = math.log1p(timeframe_mins) / math.log1p(5) roi_limits = { 'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha), 'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha), diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index fdbaaa54d..8756143a0 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -7,7 +7,7 @@ from freqtrade.exchange import timeframe_to_minutes from freqtrade.strategy.interface import SellType ticker_start_time = arrow.get(2018, 10, 3) -tests_ticker_interval = '1h' +tests_timeframe = '1h' class BTrade(NamedTuple): @@ -36,7 +36,7 @@ class BTContainer(NamedTuple): def _get_frame_time_from_offset(offset): - return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_ticker_interval)) + return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_timeframe)) ).datetime diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 54f4c8796..3f6cc8c9a 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -9,7 +9,7 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, - _get_frame_time_from_offset, tests_ticker_interval) + _get_frame_time_from_offset, tests_timeframe) # Test 0: Sell with signal sell in candle 3 # Test with Stop-loss at 1% @@ -293,7 +293,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: """ default_conf["stoploss"] = data.stop_loss default_conf["minimal_roi"] = data.roi - default_conf["ticker_interval"] = tests_ticker_interval + default_conf["ticker_interval"] = tests_timeframe default_conf["trailing_stop"] = data.trailing_stop default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached # Only add this to configuration If it's necessary diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index a5ab6d84c..508c12e89 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -307,7 +307,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) assert backtesting.config == default_conf - assert backtesting.ticker_interval == '5m' + assert backtesting.timeframe == '5m' assert callable(backtesting.strategy.tickerdata_to_dataframe) assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_sell) From 2eb651325108c9602601c7cd37538ab4886074d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 15:43:10 +0100 Subject: [PATCH 197/319] Improve timedout handling --- freqtrade/freqtradebot.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7e9706803..512fc4061 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -139,10 +139,9 @@ class FreqtradeBot: if len(trades) < self.config['max_open_trades']: self.process_maybe_execute_buys() - if 'unfilledtimeout' in self.config: - # Check and handle any timed out open orders - self.check_handle_timedout() - Trade.session.flush() + # Check and handle any timed out open orders + self.check_handle_timedout() + Trade.session.flush() if (self.heartbeat_interval and (arrow.utcnow().timestamp - self._heartbeat_msg > self.heartbeat_interval)): @@ -756,23 +755,28 @@ class FreqtradeBot: return True return False + def _check_timed_out(self, side: str, order: dict) -> bool: + """ + Check if timeout is active, and if the order is still open and timed out + """ + timeout = self.config.get('unfilledtimeout', {}).get(side) + ordertime = arrow.get(order['datetime']).datetime + if timeout: + timeout_threshold = arrow.utcnow().shift(minutes=-timeout).datetime + + return (order['status'] == 'open' and order['side'] == side + and ordertime < timeout_threshold) + return False + def check_handle_timedout(self) -> None: """ Check if any orders are timed out and cancel if neccessary :param timeoutvalue: Number of minutes until order is considered timed out :return: None """ - buy_timeout = self.config['unfilledtimeout']['buy'] - sell_timeout = self.config['unfilledtimeout']['sell'] - buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime - sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime for trade in Trade.get_open_order_trades(): try: - # FIXME: Somehow the query above returns results - # where the open_order_id is in fact None. - # This is probably because the record got - # updated via /forcesell in a different thread. if not trade.open_order_id: continue order = self.exchange.get_order(trade.open_order_id, trade.pair) @@ -782,7 +786,6 @@ class FreqtradeBot: trade, traceback.format_exc()) continue - ordertime = arrow.get(order['datetime']).datetime # Check if trade is still actually open if float(order['remaining']) == 0.0: @@ -790,15 +793,13 @@ class FreqtradeBot: continue if ((order['side'] == 'buy' and order['status'] == 'canceled') - or (order['status'] == 'open' - and order['side'] == 'buy' and ordertime < buy_timeout_threshold)): + or (self._check_timed_out('buy', order))): self.handle_timedout_limit_buy(trade, order) self.wallets.update() elif ((order['side'] == 'sell' and order['status'] == 'canceled') - or (order['status'] == 'open' - and order['side'] == 'sell' and ordertime < sell_timeout_threshold)): + or (self._check_timed_out('sell', order))): self.handle_timedout_limit_sell(trade, order) self.wallets.update() From 5b62ad876e0792cdd98909ef6904d9ef5bcd6376 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Nov 2019 09:38:06 +0100 Subject: [PATCH 198/319] Remove hyperopts occurances --- .travis.yml | 2 +- docs/bot-usage.md | 4 ++-- freqtrade/configuration/cli_options.py | 4 ++-- freqtrade/optimize/__init__.py | 2 +- freqtrade/optimize/hyperopt_interface.py | 6 +++--- freqtrade/optimize/hyperopt_loss_interface.py | 4 ++-- freqtrade/resolvers/hyperopt_resolver.py | 2 +- freqtrade/utils.py | 2 +- user_data/hyperopts/sample_hyperopt.py | 2 +- user_data/hyperopts/sample_hyperopt_advanced.py | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index a5a8093ec..8aaff553f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ jobs: name: backtest - script: - cp config.json.example config.json - - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpts + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt name: hyperopt - script: flake8 name: flake8 diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8c85965a4..b88e33bd5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -131,7 +131,7 @@ You can add the entry "user_data_dir" setting to your configuration, to always p 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. +This directory should contain your custom strategies, custom hyperopt and hyperopt loss functions, backtesting historical data (downloaded using either backtesting command or the download script) and plot outputs. It is recommended to use version control to keep track of changes to your strategies. @@ -294,7 +294,7 @@ optional arguments: entry and exit). --hyperopt NAME Specify hyperopt class name which will be used by the bot. - --hyperopt-path PATH Specify additional lookup path for Hyperopts and + --hyperopt-path PATH Specify additional lookup path for Hyperopt and Hyperopt Loss functions. --eps, --enable-position-stacking Allow buying the same pair multiple times (position diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index ff2178108..6dc5ef026 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -166,7 +166,7 @@ AVAILABLE_CLI_OPTIONS = { ), "hyperopt_path": Arg( '--hyperopt-path', - help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', + help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.', metavar='PATH', ), "epochs": Arg( @@ -239,7 +239,7 @@ AVAILABLE_CLI_OPTIONS = { 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. Built-in Hyperopt-loss-functions are: ' - 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss ' + 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.' '(default: `%(default)s`).', metavar='NAME', default=constants.DEFAULT_HYPEROPT_LOSS, diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 3adf5eb43..1f2f588ef 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -78,7 +78,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None: except Timeout: logger.info("Another running instance of freqtrade Hyperopt detected.") logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " - "Hyperopt module is resource hungry. Please run your Hyperopts sequentially " + "Hyperopt module is resource hungry. Please run your Hyperopt sequentially " "or on separate machines.") logger.info("Quitting now.") # TODO: return False here in order to help freqtrade to exit diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 142f305df..5cfd98632 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -1,6 +1,6 @@ """ IHyperOpt interface -This module defines the interface to apply for hyperopts +This module defines the interface to apply for hyperopt """ import logging import math @@ -27,8 +27,8 @@ def _format_exception_message(method: str, space: str) -> str: class IHyperOpt(ABC): """ - Interface for freqtrade hyperopts - Defines the mandatory structure must follow any custom hyperopts + Interface for freqtrade hyperopt + Defines the mandatory structure must follow any custom hyperopt Class attributes you can use: ticker_interval -> int: value of the ticker interval to use for the strategy diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py index b11b6e661..879a9f0e9 100644 --- a/freqtrade/optimize/hyperopt_loss_interface.py +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -1,6 +1,6 @@ """ IHyperOptLoss interface -This module defines the interface for the loss-function for hyperopts +This module defines the interface for the loss-function for hyperopt """ from abc import ABC, abstractmethod @@ -11,7 +11,7 @@ from pandas import DataFrame class IHyperOptLoss(ABC): """ - Interface for freqtrade hyperopts Loss functions. + Interface for freqtrade hyperopt Loss functions. Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) """ ticker_interval: str diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 72816a9ce..df1ff182c 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -1,7 +1,7 @@ # pragma pylint: disable=attribute-defined-outside-init """ -This module load custom hyperopts +This module load custom hyperopt """ import logging from pathlib import Path diff --git a/freqtrade/utils.py b/freqtrade/utils.py index ce1f8a7c5..ee7e3296f 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -70,7 +70,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: def start_create_userdir(args: Dict[str, Any]) -> None: """ - Create "user_data" directory to contain user data strategies, hyperopts, ...) + Create "user_data" directory to contain user data strategies, hyperopt, ...) :param args: Cli args from Arguments() :return: None """ diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 2721ab405..3be05f121 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -12,7 +12,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -class SampleHyperOpts(IHyperOpt): +class SampleHyperOpt(IHyperOpt): """ This is a sample Hyperopt to inspire you. Feel free to customize it. diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/user_data/hyperopts/sample_hyperopt_advanced.py index c5d28878c..66182edcf 100644 --- a/user_data/hyperopts/sample_hyperopt_advanced.py +++ b/user_data/hyperopts/sample_hyperopt_advanced.py @@ -14,7 +14,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -class AdvancedSampleHyperOpts(IHyperOpt): +class AdvancedSampleHyperOpt(IHyperOpt): """ This is a sample hyperopt to inspire you. Feel free to customize it. From c42c5a1f85fb38eeefa72a2b0df2da0b18fd25a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Nov 2019 10:03:59 +0100 Subject: [PATCH 199/319] Adjust "requires subcommand" message --- freqtrade/main.py | 9 ++++++--- tests/test_main.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index d984ff487..0a2adf71a 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -38,9 +38,12 @@ def main(sysargv: List[str] = None) -> None: else: # No subcommand was issued. raise OperationalException( - "Usage of freqtrade requires a subcommand.\n" - "To use the previous behaviour, run freqtrade with `freqtrade trade [...]`.\n" - "To see a full list of options, please use `freqtrade --help`" + "Usage of Freqtrade requires a subcommand to be specified.\n" + "To have the previous behavior (bot executing trades in live/dry-run modes, " + "depending on the value of the `dry_run` setting in the config), run freqtrade " + "as `freqtrade trade [options...]`.\n" + "To see the full list of options available, please use " + "`freqtrade --help` or `freqtrade --help`." ) except SystemExit as e: diff --git a/tests/test_main.py b/tests/test_main.py index dac960886..4e97c375d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -18,7 +18,7 @@ from tests.conftest import (log_has, log_has_re, patch_exchange, def test_parse_args_None(caplog) -> None: with pytest.raises(SystemExit): main([]) - assert log_has_re(r"Usage of freqtrade requires a subcommand\.", caplog) + assert log_has_re(r"Usage of Freqtrade requires a subcommand.*", caplog) def test_parse_args_backtesting(mocker) -> None: From 66619204bacefeb3f42d52a6d8f1fbd50cea5a8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Nov 2019 11:13:48 +0100 Subject: [PATCH 200/319] re-add hyperopts multiple ... --- 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 b88e33bd5..4665878d4 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -131,7 +131,7 @@ You can add the entry "user_data_dir" setting to your configuration, to always p 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 hyperopt and hyperopt loss functions, backtesting historical data (downloaded using either backtesting command 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 recommended to use version control to keep track of changes to your strategies. From 6ac73f7cde81ac04044a956138a1a627991fd1e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Nov 2019 11:28:26 +0100 Subject: [PATCH 201/319] Update missed strings --- freqtrade/data/dataprovider.py | 4 ++-- freqtrade/data/history.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index db71ff029..7b7159145 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -61,7 +61,7 @@ class DataProvider: """ Get stored historic ohlcv data :param pair: pair to get the data for - :param timeframe: ticker interval to get data for + :param timeframe: timeframe to get data for """ return load_pair_history(pair=pair, timeframe=timeframe or self._config['ticker_interval'], @@ -73,7 +73,7 @@ class DataProvider: Return pair ohlcv data, either live or cached historical -- depending on the runmode. :param pair: pair to get the data for - :param timeframe: ticker interval to get data for + :param timeframe: timeframe to get data for :return: Dataframe for this pair """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 3dea41c55..d45b1c890 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -279,7 +279,7 @@ def download_pair_history(datadir: Path, timeframe: str = '5m', timerange: Optional[TimeRange] = None) -> bool: """ - Download the latest ticker intervals from the exchange for the pair passed in parameters + Download latest candles from the exchange for the pair and timeframe passed in parameters The data is downloaded starting from the last correct data that exists in a cache. If timerange starts earlier than the data in the cache, the full data will be redownloaded From 62c1ff776e6faeda8aa6e1acc4acecaf60c84428 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Nov 2019 13:59:38 +0100 Subject: [PATCH 202/319] update action to 2.1.0 --- .github/workflows/docker_update_readme.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml index bc063617a..634517dc9 100644 --- a/.github/workflows/docker_update_readme.yml +++ b/.github/workflows/docker_update_readme.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@master - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v2.0.0 + uses: peter-evans/dockerhub-description@v2.1.0 env: DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} From e26bbc7de8e7d7d876a434f16c91d4e60ead393e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 13 Nov 2019 19:50:54 +0300 Subject: [PATCH 203/319] Add fix for bibox exchange --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bibox.py | 21 +++++++++++++++++++++ freqtrade/exchange/exchange.py | 7 +++++++ 3 files changed, 29 insertions(+) create mode 100644 freqtrade/exchange/bibox.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index c107f7abc..df18bca02 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -15,3 +15,4 @@ from freqtrade.exchange.exchange import (market_is_active, # noqa: F401 symbol_is_pair) from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401 +from freqtrade.exchange.bibox import Bibox # noqa: F401 diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py new file mode 100644 index 000000000..1f042c221 --- /dev/null +++ b/freqtrade/exchange/bibox.py @@ -0,0 +1,21 @@ +""" Bibox exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + +logger = logging.getLogger(__name__) + + +class Bibox(Exchange): + """ + Bibox exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + # Adjust ccxt exchange API metadata info + _ccxt_has: Dict = {"fetchCurrencies": False} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 05db45c9b..0dd8c4ff2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -30,6 +30,9 @@ class Exchange: _config: Dict = {} + # Adjustments to ccxt exchange API metadata info (ccxt exchange `has` options) + _ccxt_has: Dict = {} + # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} @@ -152,6 +155,10 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e + # Adjust ccxt API metadata info (`has` options) for the exchange + for k, v in self._ccxt_has.items(): + api.has[k] = v + self.set_sandbox(api, exchange_config, name) return api From 6174a5dd55aed41764de24e345e9848312879c56 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 13 Nov 2019 20:22:23 +0300 Subject: [PATCH 204/319] Reimplement adjustment of ccxt 'has' with more generic ccxt_config class attribute --- freqtrade/exchange/bibox.py | 5 +++-- freqtrade/exchange/exchange.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index 1f042c221..229abe766 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -17,5 +17,6 @@ class Bibox(Exchange): may still not work as expected. """ - # Adjust ccxt exchange API metadata info - _ccxt_has: Dict = {"fetchCurrencies": False} + # fetchCurrencies API point requires authentication for Bibox, + # so switch it off for Freqtrade load_markets() + _ccxt_config: Dict = {"has": {"fetchCurrencies": False}} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0dd8c4ff2..30868df07 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -30,8 +30,8 @@ class Exchange: _config: Dict = {} - # Adjustments to ccxt exchange API metadata info (ccxt exchange `has` options) - _ccxt_has: Dict = {} + # Parameters to add directly to ccxt sync/async initialization. + _ccxt_config: Dict = {} # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} @@ -94,10 +94,17 @@ class Exchange: self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] # Initialize ccxt objects + ccxt_config = self._ccxt_config.copy() + ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), + ccxt_config) self._api = self._init_ccxt( - exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) + exchange_config, ccxt_kwargs=ccxt_config) + + ccxt_async_config = self._ccxt_config.copy() + ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), + ccxt_async_config) self._api_async = self._init_ccxt( - exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config')) + exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) logger.info('Using Exchange "%s"', self.name) @@ -155,10 +162,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e - # Adjust ccxt API metadata info (`has` options) for the exchange - for k, v in self._ccxt_has.items(): - api.has[k] = v - self.set_sandbox(api, exchange_config, name) return api From 68904296e7db5a006b8915bd273fc09d509c0c27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Nov 2019 19:38:38 +0100 Subject: [PATCH 205/319] Allow timeout of 0 --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 512fc4061..f7cec080b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -761,7 +761,7 @@ class FreqtradeBot: """ timeout = self.config.get('unfilledtimeout', {}).get(side) ordertime = arrow.get(order['datetime']).datetime - if timeout: + if timeout is not None: timeout_threshold = arrow.utcnow().shift(minutes=-timeout).datetime return (order['status'] == 'open' and order['side'] == side From c8c48156dd7114b88780a0a119e46a443e356222 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Nov 2019 20:44:55 +0100 Subject: [PATCH 206/319] Don't load trades twice ... --- freqtrade/data/history.py | 10 +++++++--- freqtrade/plot/plotting.py | 7 ++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index d45b1c890..ec95be874 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -50,16 +50,20 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: return tickerlist[start_index:stop_index] -def trim_dataframe(df: DataFrame, timerange: TimeRange) -> DataFrame: +def trim_dataframe(df: DataFrame, timerange: TimeRange, df_date_col: str = 'date') -> DataFrame: """ Trim dataframe based on given timerange + :param df: Dataframe to trim + :param timerange: timerange (use start and end date if available) + :param: df_date_col: Column in the dataframe to use as Date column + :return: trimmed dataframe """ if timerange.starttype == 'date': start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) - df = df.loc[df['date'] >= start, :] + df = df.loc[df[df_date_col] >= start, :] if timerange.stoptype == 'date': stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc) - df = df.loc[df['date'] <= stop, :] + df = df.loc[df[df_date_col] <= stop, :] return df diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 01396aea9..6f78802ba 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -47,7 +47,7 @@ def init_plotscript(config): db_url=config.get('db_url'), exportfilename=config.get('exportfilename'), ) - + trades = history.trim_dataframe(trades, timerange, 'open_time') return {"tickers": tickers, "trades": trades, "pairs": pairs, @@ -377,10 +377,7 @@ def plot_profit(config: Dict[str, Any]) -> None: in helping out to find a good algorithm. """ plot_elements = init_plotscript(config) - trades = load_trades(config['trade_source'], - db_url=str(config.get('db_url')), - exportfilename=str(config.get('exportfilename')), - ) + trades = plot_elements['trades'] # Filter trades to relevant pairs trades = trades[trades['pair'].isin(plot_elements["pairs"])] # Create an average close price of all the pairs that were involved. From 38243c52fd2133f83299efc7bf94c2703b978209 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Nov 2019 20:45:16 +0100 Subject: [PATCH 207/319] Filter open trades - they are not added to the profit calc --- freqtrade/plot/plotting.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 6f78802ba..57a02dd6b 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -379,7 +379,12 @@ def plot_profit(config: Dict[str, Any]) -> None: plot_elements = init_plotscript(config) trades = plot_elements['trades'] # Filter trades to relevant pairs - trades = trades[trades['pair'].isin(plot_elements["pairs"])] + # Remove open pairs - we don't know the profit yet so can't calculate profit for these. + # Also, If only one open pair is left, then the profit-generation would fail. + trades = trades[(trades['pair'].isin(plot_elements["pairs"])) + & (~trades['close_time'].isnull()) + ] + # 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"], From 569a547b3f6dac3122d884bee03065371e127b1f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Nov 2019 06:49:21 +0100 Subject: [PATCH 208/319] Update Actions CI to new subcommands --- .github/workflows/ci.yml | 8 ++++---- Dockerfile.pi | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67862282a..c1112636a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,12 +72,12 @@ jobs: - name: Backtesting run: | cp config.json.example config.json - freqtrade --datadir tests/testdata backtesting + freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy - name: Hyperopt run: | cp config.json.example config.json - freqtrade --datadir tests/testdata --strategy SampleStrategy hyperopt --customhyperopt SampleHyperOpts -e 5 + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpts - name: Flake8 run: | @@ -137,12 +137,12 @@ jobs: - name: Backtesting run: | cp config.json.example config.json - freqtrade --datadir tests/testdata backtesting + freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy - name: Hyperopt run: | cp config.json.example config.json - freqtrade --datadir tests/testdata --strategy SampleStrategy hyperopt --customhyperopt SampleHyperOpts -e 5 + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --customhyperopt SampleHyperOpts - name: Flake8 run: | diff --git a/Dockerfile.pi b/Dockerfile.pi index 85ba5892f..279f85a04 100644 --- a/Dockerfile.pi +++ b/Dockerfile.pi @@ -38,3 +38,4 @@ RUN ~/berryconda3/bin/pip install -e . --no-cache-dir RUN [ "cross-build-end" ] ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"] +CMD [ "trade" ] From f94d46316e68c94036567d8fd55cecb961f3f45d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Nov 2019 06:51:02 +0100 Subject: [PATCH 209/319] update checkout action to pinned version --- .github/workflows/docker_update_readme.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml index 634517dc9..57a7e591e 100644 --- a/.github/workflows/docker_update_readme.yml +++ b/.github/workflows/docker_update_readme.yml @@ -8,7 +8,7 @@ jobs: dockerHubDescription: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v1 - name: Docker Hub Description uses: peter-evans/dockerhub-description@v2.1.0 env: From 3b9899dfd424f8f7741e8cf468a69af3c5ae8899 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Nov 2019 07:06:00 +0100 Subject: [PATCH 210/319] hyperopts ... --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1112636a..22931eeaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: - name: Hyperopt run: | cp config.json.example config.json - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpts + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt - name: Flake8 run: | @@ -142,7 +142,7 @@ jobs: - name: Hyperopt run: | cp config.json.example config.json - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --customhyperopt SampleHyperOpts + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --customhyperopt SampleHyperOpt - name: Flake8 run: | From b167fb071a68f5d3df6b9d9258cc667a7cc8da25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Nov 2019 08:44:10 +0100 Subject: [PATCH 211/319] fix windows test --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22931eeaa..04e52c0fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,7 +142,7 @@ jobs: - name: Hyperopt run: | cp config.json.example config.json - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --customhyperopt SampleHyperOpt + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt - name: Flake8 run: | From edc0d7f2c74d564b0468f27e9eb53e438284dd6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Nov 2019 20:10:17 +0100 Subject: [PATCH 212/319] Fix non-terminating bot --- freqtrade/main.py | 3 --- freqtrade/utils.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 0a2adf71a..7afaeb1a2 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -27,7 +27,6 @@ def main(sysargv: List[str] = None) -> None: """ return_code: Any = 1 - worker = None try: arguments = Arguments(sysargv) args = arguments.get_parsed_arg() @@ -57,8 +56,6 @@ def main(sysargv: List[str] = None) -> None: except Exception: logger.exception('Fatal exception!') finally: - if worker: - worker.exit() sys.exit(return_code) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 5ad134ef9..ff54790a5 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -45,8 +45,15 @@ def start_trading(args: Dict[str, Any]) -> int: """ from freqtrade.worker import Worker # Load and run worker - worker = Worker(args) - worker.run() + try: + worker = Worker(args) + worker.run() + except KeyboardInterrupt: + logger.info('SIGINT received, aborting ...') + finally: + if worker: + logger.info("worker found ... calling exit") + worker.exit() return 0 From 6e0655b3b70adf5be4af47e8a508a8bde74b3002 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Nov 2019 09:47:35 +0100 Subject: [PATCH 213/319] add empty worker variable --- freqtrade/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index ff54790a5..b8ab7504e 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -45,6 +45,7 @@ def start_trading(args: Dict[str, Any]) -> int: """ from freqtrade.worker import Worker # Load and run worker + worker = None try: worker = Worker(args) worker.run() From 91047830fd282baa72a21f0425f7e904e92ddd33 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Nov 2019 09:56:16 +0100 Subject: [PATCH 214/319] Add tst for worker termination --- tests/test_utils.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3dba7df1b..bbb4fc648 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,8 @@ from freqtrade import OperationalException from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, start_download_data, start_list_exchanges, - start_list_markets, start_list_timeframes) + start_list_markets, start_list_timeframes, + start_trading) from tests.conftest import get_args, log_has, patch_exchange @@ -24,6 +25,29 @@ def test_setup_utils_configuration(): assert config['exchange']['secret'] == '' +def test_start_trading_fail(mocker): + + mocker.patch("freqtrade.worker.Worker.run", MagicMock(side_effect=OperationalException)) + + mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(return_value=None)) + + exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock()) + args = [ + 'trade', + '-c', 'config.json.example' + ] + with pytest.raises(OperationalException): + start_trading(get_args(args)) + assert exitmock.call_count == 1 + + exitmock.reset_mock() + + mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(side_effect=OperationalException)) + with pytest.raises(OperationalException): + start_trading(get_args(args)) + assert exitmock.call_count == 0 + + def test_list_exchanges(capsys): args = [ From be53c0885df7ad29a6202502c47894c8ae3117c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Nov 2019 10:49:32 +0100 Subject: [PATCH 215/319] Try moving coveralls to github actions --- .github/workflows/ci.yml | 6 ++++++ .travis.yml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04e52c0fd..12aff89f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,9 +64,15 @@ jobs: pip install -e . - name: Tests + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + COVERALLS_SERVICE_NAME: travis-ci + TRAVIS: "true" run: | pytest --random-order --cov=freqtrade --cov-config=.coveragerc # Allow failure for coveralls + # Fake travis environment to get coveralls working correctly + export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" coveralls || true - name: Backtesting diff --git a/.travis.yml b/.travis.yml index 80e4080d3..6073e1cce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ jobs: script: - pytest --random-order --cov=freqtrade --cov-config=.coveragerc # Allow failure for coveralls - - coveralls || true + # - coveralls || true name: pytest - script: - cp config.json.example config.json From b6a12044bafa2e1fc9adc75b3fb8173313bf8091 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 10:36:32 +0100 Subject: [PATCH 216/319] seperate docs job --- .github/workflows/ci.yml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12aff89f8..f8932cf07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,10 +93,6 @@ jobs: run: | mypy freqtrade scripts - - name: Documentation syntax - run: | - ./tests/test_docs.sh - - name: Slack Notification uses: homoluctus/slatify@v1.8.0 if: always() @@ -169,8 +165,26 @@ jobs: channel: '#notifications' url: ${{ secrets.SLACK_WEBHOOK }} + docs_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - name: Documentation syntax + run: | + ./tests/test_docs.sh + + - name: Slack Notification + uses: homoluctus/slatify@v1.8.0 + if: failure() + with: + type: ${{ job.status }} + job_name: '*Freqtrade Docs*' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + deploy: - needs: [ build, build_windows ] + needs: [ build, build_windows, docs_check ] runs-on: ubuntu-18.04 if: github.event_name == 'push' || github.event_name == 'schedule' steps: From 3aee8d2b2a347d2366088119e13c65c7b656f1ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 14:40:59 +0100 Subject: [PATCH 217/319] Improve rest api client / status response --- freqtrade/rpc/api_server.py | 7 +++++-- scripts/rest_client.py | 20 ++++++++++++++------ tests/rpc/test_rpc_apiserver.py | 4 ++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 3b59c9592..851806ec2 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -330,8 +330,11 @@ class ApiServer(RPC): Returns the current status of the trades in json format """ - results = self._rpc_trade_status() - return self.rest_dump(results) + try: + results = self._rpc_trade_status() + return self.rest_dump(results) + except RPCException: + return self.rest_dump([]) @require_login @rpc_catch_errors diff --git a/scripts/rest_client.py b/scripts/rest_client.py index a46b3ebfb..096286013 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -8,12 +8,14 @@ so it can be used as a standalone script. """ import argparse +import inspect import json import logging -import inspect -from urllib.parse import urlencode, urlparse, urlunparse +import sys from pathlib import Path +from urllib.parse import urlencode, urlparse, urlunparse +import rapidjson import requests from requests.exceptions import ConnectionError @@ -190,7 +192,9 @@ class FtRestClient(): def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", - help="Positional argument defining the command to execute.") + help="Positional argument defining the command to execute.", + nargs="?" + ) parser.add_argument('--show', help='Show possible methods with this client', @@ -221,9 +225,12 @@ def load_config(configfile): file = Path(configfile) if file.is_file(): with file.open("r") as f: - config = json.load(f) + config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS | + rapidjson.PM_TRAILING_COMMAS) return config - return {} + else: + logger.warning(f"Could not load config file {file}.") + sys.exit(1) def print_commands(): @@ -237,8 +244,9 @@ def print_commands(): def main(args): - if args.get("help"): + if args.get("show"): print_commands() + sys.exit() config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 6e65cf934..cbca7e3d5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -417,8 +417,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets): ) rc = client_get(client, f"{BASE_URI}/status") - assert_response(rc, 502) - assert rc.json == {'error': 'Error querying _status: no active trade'} + assert_response(rc, 200) + assert rc.json == [] ftbot.create_trades() rc = client_get(client, f"{BASE_URI}/status") From 2c976bdd24b00ee448463d3399cc95d9e183d115 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 14:56:08 +0100 Subject: [PATCH 218/319] Add show_config endpoint --- freqtrade/rpc/api_server.py | 10 ++++++++++ freqtrade/rpc/rpc.py | 20 ++++++++++++++++++++ scripts/rest_client.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 12 ++++++++++++ 4 files changed, 49 insertions(+) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 3b59c9592..0426eb598 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -169,6 +169,8 @@ class ApiServer(RPC): view_func=self._status, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/version', 'version', view_func=self._version, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/show_config', 'show_config', + view_func=self._show_config, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/ping', 'ping', view_func=self._ping, methods=['GET']) @@ -241,6 +243,14 @@ class ApiServer(RPC): """ return self.rest_dump({"version": __version__}) + @require_login + @rpc_catch_errors + def _show_config(self): + """ + Prints the bot's version + """ + return self.rest_dump(self._rpc_show_config()) + @require_login @rpc_catch_errors def _reload_conf(self): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5ab92cf33..c78951a6d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -80,6 +80,26 @@ class RPC: def send_msg(self, msg: Dict[str, str]) -> None: """ Sends a message to all registered rpc modules """ + def _rpc_show_config(self) -> Dict: + """ + Return a dict of config options. + Explicitly does NOT return the full config to avoid leakage of sensitive + information via rpc. + """ + config = self._freqtrade.config + val = { + 'dry_run': config.get('dry_run', False), + 'stake_currency': config['stake_currency'], + 'stake_amount': config['stake_amount'], + 'minimal_roi': config['minimal_roi'].copy(), + 'stoploss': config['stoploss'], + 'trailing_stop': config['trailing_stop'], + 'ticker_interval': config['ticker_interval'], + 'exchange': config['exchange']['name'], + 'strategy': config['strategy'], + } + return val + def _rpc_trade_status(self) -> List[Dict[str, Any]]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is diff --git a/scripts/rest_client.py b/scripts/rest_client.py index a46b3ebfb..7408acab8 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -147,6 +147,13 @@ class FtRestClient(): """ return self._get("version") + def show_config(self): + """ + Returns part of the configuration, relevant for trading operations. + :return: json object containing the version + """ + return self._get("show_config") + def whitelist(self): """ Show the current whitelist diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 6e65cf934..d1ebe961b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -284,6 +284,18 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json["max"] == 1.0 +def test_api_show_config(botclient, mocker): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + + rc = client_get(client, f"{BASE_URI}/show_config") + assert_response(rc) + assert 'dry_run' in rc.json + assert rc.json['exchange'] == 'bittrex' + assert rc.json['ticker_interval'] == '5m' + assert not rc.json['trailing_stop'] + + def test_api_daily(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From acab56793f1253dc46252a3bf6afafd6081c453a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 15:03:45 +0100 Subject: [PATCH 219/319] Add /show_config to telegram --- freqtrade/rpc/telegram.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8a81848ac..ba475e39e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -101,6 +101,7 @@ class Telegram(RPC): CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), + CommandHandler('show_config', self._show_config), ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -550,6 +551,7 @@ class Telegram(RPC): "*/balance:* `Show account balance per currency`\n" \ "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ "*/reload_conf:* `Reload configuration file` \n" \ + "*/show_config:* `Show running configuration` \n" \ "*/whitelist:* `Show current whitelist` \n" \ "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \ "to the blacklist.` \n" \ @@ -570,6 +572,26 @@ class Telegram(RPC): """ self._send_msg('*Version:* `{}`'.format(__version__)) + @authorized_only + def _show_config(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /show_config. + Show config information information + :param bot: telegram bot + :param update: message update + :return: None + """ + val = self._rpc_show_config() + self._send_msg( + f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" + f"*Exchange:* `{val['exchange']}`\n" + f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" + f"*Minimum ROI:* `{val['minimal_roi']}`\n" + f"*{'Trailing ' if val['trailing_stop'] else ''}Stoploss:* `{val['stoploss']}`\n" + f"*Ticker Interval:* `{val['ticker_interval']}`\n" + f"*Strategy:* `{val['strategy']}`'" + ) + def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: """ Send given markdown message From 2b190e5638d8f65b92d3b3ef75bb34324bc8db35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 15:05:56 +0100 Subject: [PATCH 220/319] Add documentation --- docs/rest-api.md | 5 +++++ docs/telegram-usage.md | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index 70e090569..187a71c97 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -106,6 +106,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `stop` | | Stops the trader | `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `reload_conf` | | Reloads the configuration file +| `show_config` | | Shows part of the current configuration with relevant settings to operation | `status` | | Lists all open trades | `count` | | Displays number of trades used and available | `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance @@ -172,6 +173,10 @@ reload_conf Reload configuration :returns: json object +show_config + Returns part of the configuration, relevant for trading operations. + :return: json object containing the version + start Start the bot if it's in stopped state. :returns: json object diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 424b55faf..ed0c21a6e 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -53,6 +53,7 @@ official commands. You can ask at any moment for help with `/help`. | `/stop` | | Stops the trader | `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/reload_conf` | | Reloads the configuration file +| `/show_config` | | Shows part of the current configuration with relevant settings to operation | `/status` | | Lists all open trades | `/status table` | | List all open trades in a table format | `/count` | | Displays number of trades used and available From e4e8a611be888e657ebdd5f7c5b61ac71846d388 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 15:12:53 +0100 Subject: [PATCH 221/319] Add tests for telegram --- freqtrade/rpc/rpc.py | 5 ++++- tests/rpc/test_rpc_telegram.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c78951a6d..8b557df96 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -80,7 +80,7 @@ class RPC: def send_msg(self, msg: Dict[str, str]) -> None: """ Sends a message to all registered rpc modules """ - def _rpc_show_config(self) -> Dict: + def _rpc_show_config(self) -> Dict[str, Any]: """ Return a dict of config options. Explicitly does NOT return the full config to avoid leakage of sensitive @@ -94,6 +94,9 @@ class RPC: 'minimal_roi': config['minimal_roi'].copy(), 'stoploss': config['stoploss'], 'trailing_stop': config['trailing_stop'], + 'trailing_stop_positive': config.get('trailing_stop_positive'), + 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), + 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'ticker_interval': config['ticker_interval'], 'exchange': config['exchange']['name'], 'strategy': config['strategy'], diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 766511d2d..73b38b808 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1174,6 +1174,23 @@ def test_version_handle(default_conf, update, mocker) -> None: assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] +def test_show_config_handle(default_conf, update, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + + telegram._show_config(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] + + def test_send_msg_buy_notification(default_conf, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( From 547d65b06511515952dfa49156c4ab3ccbd75267 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 15:22:14 +0100 Subject: [PATCH 222/319] Fix broken test --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ba475e39e..0547af7b0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -95,13 +95,13 @@ class Telegram(RPC): CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('reload_conf', self._reload_conf), + CommandHandler('show_config', self._show_config), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), CommandHandler('blacklist', self._blacklist), CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), - CommandHandler('show_config', self._show_config), ] for handle in handles: self._updater.dispatcher.add_handler(handle) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 73b38b808..c6a8094ab 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -73,7 +73,7 @@ def test_init(default_conf, mocker, caplog) -> None: message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \ - "['performance'], ['daily'], ['count'], ['reload_conf'], " \ + "['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " \ "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]" assert log_has(message_str, caplog) From 599e18b9209adf9ea16cbb9ecd144fb6b4122bb9 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:20:02 +0000 Subject: [PATCH 223/319] Bump urllib3 from 1.25.6 to 1.25.7 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.6 to 1.25.7. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.6...1.25.7) 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 33a5d0776..a4521af71 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -6,7 +6,7 @@ python-telegram-bot==12.2.0 arrow==0.15.4 cachetools==3.1.1 requests==2.22.0 -urllib3==1.25.6 +urllib3==1.25.7 wrapt==1.11.2 jsonschema==3.1.1 TA-Lib==0.4.17 From 933564591dfc99ddd0473aebbd2634e742abe8c8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:20:30 +0000 Subject: [PATCH 224/319] Bump sqlalchemy from 1.3.10 to 1.3.11 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.10 to 1.3.11. - [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 33a5d0776..cbfcd8035 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.19.25 -SQLAlchemy==1.3.10 +SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 cachetools==3.1.1 From 42474b714422141612d76cc21432d871bc85c1e6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:20:50 +0000 Subject: [PATCH 225/319] Bump flake8-tidy-imports from 3.0.0 to 3.1.0 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases) - [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst) - [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/3.0.0...3.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 f346439af..ca51a98d4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==1.8.2 flake8==3.7.9 flake8-type-annotations==0.1.0 -flake8-tidy-imports==3.0.0 +flake8-tidy-imports==3.1.0 mypy==0.740 pytest==5.2.2 pytest-asyncio==0.10.0 From a33d4087809e8ecacc145b702568a371ccbd6e2a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:21:25 +0000 Subject: [PATCH 226/319] Bump plotly from 4.2.1 to 4.3.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.2.1 to 4.3.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.2.1...v4.3.0) Signed-off-by: dependabot-preview[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 235c71896..87d5553b6 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.2.1 +plotly==4.3.0 From e7157faddd24ac74264998999be8ddecb95a1505 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:22:18 +0000 Subject: [PATCH 227/319] Bump python-rapidjson from 0.8.0 to 0.9.1 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 0.8.0 to 0.9.1. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v0.8.0...v0.9.1) 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 33a5d0776..635b3aafe 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -17,7 +17,7 @@ coinmarketcap==5.0.3 py_find_1st==1.1.4 # Load ticker files 30% faster -python-rapidjson==0.8.0 +python-rapidjson==0.9.1 # Notify systemd sdnotify==0.3.2 From cb6b3e17a936c1496e61dfc6f1bbc4185b51d954 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:22:41 +0000 Subject: [PATCH 228/319] Bump tabulate from 0.8.5 to 0.8.6 Bumps [tabulate](https://github.com/astanin/python-tabulate) from 0.8.5 to 0.8.6. - [Release notes](https://github.com/astanin/python-tabulate/releases) - [Changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG) - [Commits](https://github.com/astanin/python-tabulate/compare/v0.8.5...v0.8.6) 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 33a5d0776..1215521a9 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -10,7 +10,7 @@ urllib3==1.25.6 wrapt==1.11.2 jsonschema==3.1.1 TA-Lib==0.4.17 -tabulate==0.8.5 +tabulate==0.8.6 coinmarketcap==5.0.3 # find first, C search in arrays From 0bc71403ff87a2918a912c992d3e263422f0a85c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:24:39 +0000 Subject: [PATCH 229/319] Bump mkdocs-material from 4.4.3 to 4.5.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 4.4.3 to 4.5.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/4.4.3...4.5.0) Signed-off-by: dependabot-preview[bot] --- 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 5e7fe7084..bfa5c0d1e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==4.4.3 +mkdocs-material==4.5.0 mdx_truly_sane_lists==1.2 From dddccf8f1af791cd8a7a6d6e3a36c33e08f4f987 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:39:12 +0000 Subject: [PATCH 230/319] Bump ccxt from 1.19.25 to 1.19.54 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.19.25 to 1.19.54. - [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.19.25...1.19.54) 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 7997506ce..a0e9b2a0c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.19.25 +ccxt==1.19.54 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 From cd6d2761197659b02463ca91497816c8ec4d963f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:42:57 +0000 Subject: [PATCH 231/319] Bump pytest from 5.2.2 to 5.2.4 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.2.2 to 5.2.4. - [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.2.2...5.2.4) 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 ca51a98d4..297b95623 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.1.0 mypy==0.740 -pytest==5.2.2 +pytest==5.2.4 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==1.11.2 From c22b00b30303ba44f3dcf140b4fd8b70fb2e2f80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Nov 2019 06:34:54 +0100 Subject: [PATCH 232/319] move pairlist filters out of config[] --- config_full.json.example | 5 +---- docs/configuration.md | 22 +++++++------------- freqtrade/pairlist/LowPriceFilter.py | 8 +++---- freqtrade/pairlist/pairlistmanager.py | 2 +- tests/pairlist/test_pairlist.py | 30 +++++++++++++-------------- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_telegram.py | 2 +- 7 files changed, 30 insertions(+), 41 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index ba53f47d6..270938237 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -61,10 +61,7 @@ } }, {"method": "PrecisionFilter"}, - {"method": "LowPriceFilter", - "config": { - "low_price_percent": 0.01 - } + {"method": "LowPriceFilter", "low_price_ratio": 0.01 } ], "exchange": { diff --git a/docs/configuration.md b/docs/configuration.md index 8a2b74ed5..120842cdb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -416,11 +416,9 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis ```json "pairlists": [{ "method": "VolumePairList", - "config": { - "number_assets": 20, - "sort_key": "quoteVolume", - "ttl": 1800, - } + "number_assets": 20, + "sort_key": "quoteVolume", + "ttl": 1800, ], ``` @@ -430,7 +428,7 @@ Filters low-value coins which would not allow setting a stoploss. #### Low Price Pair Filter -The `LowPriceFilter` allows filtering of pairs where a raise of 1 price unit is below the `low_price_percent` ratio. +The `LowPriceFilter` allows filtering of pairs where a raise of 1 price unit is below the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to <> 0. Calculation example: @@ -450,16 +448,12 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, "pairlists": [ { "method": "VolumePairList", - "config": { - "number_assets": 20, - "sort_key": "quoteVolume", - }, + "number_assets": 20, + "sort_key": "quoteVolume", }, {"method": "PrecisionFilter"}, - {"method": "LowPriceFilter", - "config": {"low_price_percent": 0.01} - } - }], + {"method": "LowPriceFilter", "low_price_ratio": 0.01} + ], ``` ## Switch to Dry-run mode diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/LowPriceFilter.py index 83b6a85e6..ff18b97c8 100644 --- a/freqtrade/pairlist/LowPriceFilter.py +++ b/freqtrade/pairlist/LowPriceFilter.py @@ -13,7 +13,7 @@ class LowPriceFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - self._low_price_percent = pairlistconfig.get('low_price_percent', 0) + self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) @property def needstickers(self) -> bool: @@ -28,7 +28,7 @@ class LowPriceFilter(IPairList): """ Short whitelist method description - used for startup-messages """ - return f"{self.name} - Filtering pairs priced below {self._low_price_percent * 100}%." + return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%." def _validate_ticker_lowprice(self, ticker) -> bool: """ @@ -41,7 +41,7 @@ class LowPriceFilter(IPairList): compare = ticker['last'] + 1 / pow(10, precision) changeperc = (compare - ticker['last']) / ticker['last'] - if changeperc > self._low_price_percent: + if changeperc > self._low_price_ratio: logger.info(f"Removed {ticker['symbol']} from whitelist, " f"because 1 unit is {changeperc * 100:.3f}%") return False @@ -63,7 +63,7 @@ class LowPriceFilter(IPairList): pairlist.remove(p) # Filter out assets which would not allow setting a stoploss - if self._low_price_percent and not self._validate_ticker_lowprice(ticker): + if self._low_price_ratio and not self._validate_ticker_lowprice(ticker): pairlist.remove(p) return pairlist diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 0734d7f8f..fa5382c37 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -32,7 +32,7 @@ class PairListManager(): exchange=exchange, pairlistmanager=self, config=config, - pairlistconfig=pl.get('config'), + pairlistconfig=pl, pairlist_pos=len(self._pairlists) ).pairlist self._tickers_needed = pairl.needstickers or self._tickers_needed diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 13f868c7a..9daaa5353 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -29,10 +29,8 @@ def whitelist_conf(default_conf): default_conf['pairlists'] = [ { "method": "VolumePairList", - "config": { - "number_assets": 5, - "sort_key": "quoteVolume", - } + "number_assets": 5, + "sort_key": "quoteVolume", }, ] return default_conf @@ -136,37 +134,37 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): @pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [ - ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}], + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), # Different sorting depending on quote or bid volume - ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "bidVolume"}}], + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], "BTC", ['HOT/BTC', 'FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']), - ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}], + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "USDT", ['ETH/USDT']), # No pair for ETH ... - ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}], + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "ETH", []), # Precisionfilter and quote volume - ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}, + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']), # Precisionfilter bid - ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "bidVolume"}}, + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, {"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']), # Lowpricefilter and VolumePairList - ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}, - {"method": "LowPriceFilter", "config": {"low_price_percent": 0.03}}], + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "LowPriceFilter", "low_price_ratio": 0.03}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']), # Hot is removed by precision_filter, Fuel by low_price_filter. - ([{"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "quoteVolume"}}, + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}, - {"method": "LowPriceFilter", "config": {"low_price_percent": 0.02}} + {"method": "LowPriceFilter", "low_price_ratio": 0.02} ], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), # StaticPairlist Only ([{"method": "StaticPairList"}, ], "BTC", ['ETH/BTC', 'TKN/BTC']), # Static Pairlist before VolumePairList - sorting changes ([{"method": "StaticPairList"}, - {"method": "VolumePairList", "config": {"number_assets": 5, "sort_key": "bidVolume"}}, + {"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, ], "BTC", ['TKN/BTC', 'ETH/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, @@ -257,7 +255,7 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf): - whitelist_conf['pairlists'][0]['config'].update({"sort_key": "asdf"}) + whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) with pytest.raises(OperationalException, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 8747fe6ff..edc0fdb8a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -726,7 +726,7 @@ def test_rpc_whitelist(mocker, default_conf) -> None: def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', - 'config': {'number_assets': 4} + 'number_assets': 4, }] mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bb9d88658..52d58e887 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1063,7 +1063,7 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: ) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) default_conf['pairlists'] = [{'method': 'VolumePairList', - 'config': {'number_assets': 4} + 'number_assets': 4 }] freqtradebot = get_patched_freqtradebot(mocker, default_conf) From a8855bf7955eed07a107e31ca53b0a418426c373 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Nov 2019 06:41:05 +0100 Subject: [PATCH 233/319] rename LowPriceFilter to PrieFilter --- config_full.json.example | 2 +- docs/configuration.md | 13 +++++++------ freqtrade/constants.py | 2 +- .../pairlist/{LowPriceFilter.py => PriceFilter.py} | 4 ++-- tests/pairlist/test_pairlist.py | 8 ++++---- 5 files changed, 15 insertions(+), 14 deletions(-) rename freqtrade/pairlist/{LowPriceFilter.py => PriceFilter.py} (96%) diff --git a/config_full.json.example b/config_full.json.example index 270938237..5ceeaff09 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -61,7 +61,7 @@ } }, {"method": "PrecisionFilter"}, - {"method": "LowPriceFilter", "low_price_ratio": 0.01 + {"method": "PriceFilter", "low_price_ratio": 0.01 } ], "exchange": { diff --git a/docs/configuration.md b/docs/configuration.md index 120842cdb..ed50b521e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -380,7 +380,7 @@ The valid values are: Pairlists define the list of pairs that the bot should trade. There are [`StaticPairList`](#static-pair-list) and dynamic Whitelists available. -[`PrecisionFilter`](#precision-filter) and [`LowPriceFilter`](#low-price-pair-filter) act as filters, removing low-value pairs. +[`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter) act as filters, removing low-value pairs. All pairlists can be chained, and a combination of all pairlists will become your new whitelist. Pairlists are executed in the sequence they are configured. You should always configure either `StaticPairList` or `DynamicPairList` as starting pairlists. @@ -391,7 +391,7 @@ Inactive markets and blacklisted pairs are always removed from the resulting `pa * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) * [`PrecisionFilter`](#precision-filter) -* [`LowPriceFilter`](#low-price-pair-filter) +* [`PriceFilter`](#price-pair-filter) #### Static Pair List @@ -426,9 +426,10 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis Filters low-value coins which would not allow setting a stoploss. -#### Low Price Pair Filter +#### Price Pair Filter -The `LowPriceFilter` allows filtering of pairs where a raise of 1 price unit is below the `low_price_ratio` ratio. +The `PriceFilter` allows filtering of pairs by price. +Currently, only `low_price_ratio` is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to <> 0. Calculation example: @@ -438,7 +439,7 @@ These pairs are dangerous since it may be impossible to place the desired stoplo ### Full Pairlist example -The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`LowPriceFilter`](#low-price-pair-filter), filtering all assets where 1 priceunit is > 1%. +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter), filtering all assets where 1 priceunit is > 1%. ```json "exchange": { @@ -452,7 +453,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, "sort_key": "quoteVolume", }, {"method": "PrecisionFilter"}, - {"method": "LowPriceFilter", "low_price_ratio": 0.01} + {"method": "PriceFilter", "low_price_ratio": 0.01} ], ``` diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c98c32d4c..24e3589fd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -20,7 +20,7 @@ REQUIRED_ORDERTIF = ['buy', 'sell'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] -AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'LowPriceFilter'] +AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter'] DRY_RUN_WALLET = 999.9 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons diff --git a/freqtrade/pairlist/LowPriceFilter.py b/freqtrade/pairlist/PriceFilter.py similarity index 96% rename from freqtrade/pairlist/LowPriceFilter.py rename to freqtrade/pairlist/PriceFilter.py index ff18b97c8..b3546ebd9 100644 --- a/freqtrade/pairlist/LowPriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -7,7 +7,7 @@ from freqtrade.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) -class LowPriceFilter(IPairList): +class PriceFilter(IPairList): def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict, pairlist_pos: int) -> None: @@ -32,7 +32,7 @@ class LowPriceFilter(IPairList): def _validate_ticker_lowprice(self, ticker) -> bool: """ - Check if if one price-step is > than a certain barrier. + Check if if one price-step (pip) is > than a certain barrier. :param ticker: ticker dict as returned from ccxt.load_markets() :param precision: Precision :return: True if the pair can stay, false if it should be removed diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 9daaa5353..76537880c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -150,14 +150,14 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # Precisionfilter bid ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, {"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']), - # Lowpricefilter and VolumePairList + # PriceFilter and VolumePairList ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "LowPriceFilter", "low_price_ratio": 0.03}], + {"method": "PriceFilter", "low_price_ratio": 0.03}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']), # Hot is removed by precision_filter, Fuel by low_price_filter. ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}, - {"method": "LowPriceFilter", "low_price_ratio": 0.02} + {"method": "PriceFilter", "low_price_ratio": 0.02} ], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), # StaticPairlist Only ([{"method": "StaticPairList"}, @@ -189,7 +189,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t if pairlist['method'] == 'PrecisionFilter': assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' r'would be <= stop limit.*', caplog) - if pairlist['method'] == 'LowPriceFilter': + if pairlist['method'] == 'PriceFilter': assert log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) From 5f62a9e4d8eed5d3c7a15eca92c041c84bf12b57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Nov 2019 06:48:56 +0100 Subject: [PATCH 234/319] rename ttl to refresh_period --- config_full.json.example | 8 +++----- docs/configuration.md | 4 ++-- docs/developer.md | 2 +- freqtrade/pairlist/VolumePairList.py | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 5ceeaff09..b9631f63d 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -54,11 +54,9 @@ {"method": "StaticPairList"}, { "method": "VolumePairList", - "config": { - "number_assets": 20, - "sort_key": "quoteVolume", - "ttl": 1800 - } + "number_assets": 20, + "sort_key": "quoteVolume", + "refresh_period": 1800 }, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01 diff --git a/docs/configuration.md b/docs/configuration.md index ed50b521e..946ee3bf7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -411,14 +411,14 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis `VolumePairList` considers outputs of previous pairlists unless it's the first configured pairlist, it does not consider `pair_whitelist`, but selects the top assets from all available markets (with matching stake-currency) on the exchange. -`ttl` allows setting the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). +`refresh_period` allows setting the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). ```json "pairlists": [{ "method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume", - "ttl": 1800, + "refresh_period": 1800, ], ``` diff --git a/docs/developer.md b/docs/developer.md index 0eba6fd59..2d39893bc 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -115,7 +115,7 @@ Now, let's step through the methods which require actions: #### Pairlist configuration Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`. -This Pairlist-object may contain a `"config"` dict with additional configurations for the configured pairlist. +This Pairlist-object may contain configurations with additional configurations for the configured pairlist. By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the whitelist. Please follow this to ensure a consistent user experience. Additional elements can be configured as needed. `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic. diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 708c8d7c2..2df9ba691 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -28,7 +28,7 @@ class VolumePairList(IPairList): 'for "pairlist.config.number_assets"') self._number_pairs = self._pairlistconfig['number_assets'] self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') - self._ttl = self._pairlistconfig.get('ttl', 1800) + self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) if not self._exchange.exchange_has('fetchTickers'): raise OperationalException( @@ -67,7 +67,7 @@ class VolumePairList(IPairList): :return: new whitelist """ # Generate dynamic whitelist - if self._last_refresh + self._ttl < datetime.now().timestamp(): + if self._last_refresh + self.refresh_period < datetime.now().timestamp(): self._last_refresh = int(datetime.now().timestamp()) return self._gen_pair_whitelist(pairlist, tickers, From 751157b4ea3ac291e904a01ed5319bec3d680d44 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 18 Nov 2019 19:32:35 +0100 Subject: [PATCH 235/319] Don't notify on builds from forks they don't have secrets available ATM --- .github/workflows/ci.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8932cf07..c74b1720e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,8 @@ jobs: # Allow failure for coveralls # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" + export CI_BRANCH=${GITHUB_REF#"ref/heads"} + echo "${CI_BRANCH}" coveralls || true - name: Backtesting @@ -95,7 +97,7 @@ jobs: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 - if: always() + if: always() && github.repository.fork == true with: type: ${{ job.status }} job_name: '*Freqtrade CI ${{ matrix.os }}*' @@ -156,7 +158,7 @@ jobs: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 - if: always() + if: always() && github.repository.fork == true with: type: ${{ job.status }} job_name: '*Freqtrade CI windows*' @@ -176,7 +178,7 @@ jobs: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 - if: failure() + if: failure() && github.repository.fork == true with: type: ${{ job.status }} job_name: '*Freqtrade Docs*' @@ -186,7 +188,7 @@ jobs: deploy: needs: [ build, build_windows, docs_check ] runs-on: ubuntu-18.04 - if: github.event_name == 'push' || github.event_name == 'schedule' + if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade' steps: - uses: actions/checkout@v1 @@ -217,7 +219,7 @@ jobs: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 - if: always() + if: always() && github.repository.fork == true with: type: ${{ job.status }} job_name: '*Freqtrade CI Deploy*' From c92f233c1565174b4b8aa6f0ef2bbd57e1b1db2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Nov 2019 19:33:04 +0100 Subject: [PATCH 236/319] Move settings to correct location --- freqtrade/configuration/deprecated_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 3aec85ae2..8f3dbd675 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -63,9 +63,9 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: "DEPRECATED: " f"Using VolumePairList in pairlist is deprecated and must be moved to pairlists. " "Please refer to the docs on configuration details") - config['pairlists'].append({'method': 'VolumePairList', - 'config': config.get('pairlist', {}).get('config') - }) + pl = {'method': 'VolumePairList'} + pl.update(config.get('pairlist', {}).get('config')) + config['pairlists'].append(pl) if config.get('pairlist', {}).get('config', {}).get('precision_filter'): logger.warning( From 633996216a2640a19454aa8bbdd40c86021bbbac Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Nov 2019 15:20:39 +0300 Subject: [PATCH 237/319] Improve commands help list --- scripts/rest_client.py | 69 +++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 096286013..03e4fc76b 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -65,100 +65,99 @@ class FtRestClient(): return self._call("POST", apipath, params=params, data=data) def start(self): - """ - Start the bot if it's in stopped state. + """Start the bot if it's in the stopped state. + :return: json object """ return self._post("start") def stop(self): - """ - Stop the bot. Use start to restart + """Stop the bot. Use `start` to restart. + :return: json object """ return self._post("stop") def stopbuy(self): - """ - Stop buying (but handle sells gracefully). - use reload_conf to reset + """Stop buying (but handle sells gracefully). Use `reload_conf` to reset. + :return: json object """ return self._post("stopbuy") def reload_conf(self): - """ - Reload configuration + """Reload configuration. + :return: json object """ return self._post("reload_conf") def balance(self): - """ - Get the account balance + """Get the account balance. + :return: json object """ return self._get("balance") def count(self): - """ - Returns the amount of open trades + """Return the amount of open trades. + :return: json object """ return self._get("count") def daily(self, days=None): - """ - Returns the amount of open trades + """Return the amount of open trades. + :return: json object """ return self._get("daily", params={"timescale": days} if days else None) def edge(self): - """ - Returns information about edge + """Return information about edge. + :return: json object """ return self._get("edge") def profit(self): - """ - Returns the profit summary + """Return the profit summary. + :return: json object """ return self._get("profit") def performance(self): - """ - Returns the performance of the different coins + """Return the performance of the different coins. + :return: json object """ return self._get("performance") def status(self): - """ - Get the status of open trades + """Get the status of open trades. + :return: json object """ return self._get("status") def version(self): - """ - Returns the version of the bot + """Return the version of the bot. + :return: json object containing the version """ return self._get("version") def whitelist(self): - """ - Show the current whitelist + """Show the current whitelist. + :return: json object """ return self._get("whitelist") def blacklist(self, *args): - """ - Show the current blacklist + """Show the current blacklist. + :param add: List of coins to add (example: "BNB/BTC") :return: json object """ @@ -168,8 +167,8 @@ class FtRestClient(): return self._post("blacklist", data={"blacklist": args}) def forcebuy(self, pair, price=None): - """ - Buy an asset + """Buy an asset. + :param pair: Pair to buy (ETH/BTC) :param price: Optional - price to buy :return: json object of the trade @@ -180,8 +179,8 @@ class FtRestClient(): return self._post("forcebuy", data=data) def forcesell(self, tradeid): - """ - Force-sell a trade + """Force-sell a trade. + :param tradeid: Id of the trade (can be received via status command) :return: json object """ @@ -236,10 +235,10 @@ def load_config(configfile): def print_commands(): # Print dynamic help for the different commands using the commands doc-strings client = FtRestClient(None) - print("Possible commands:") + print("Possible commands:\n") for x, y in inspect.getmembers(client): if not x.startswith('_'): - print(f"{x} {getattr(client, x).__doc__}") + print(f"{x}""\n "f"{getattr(client, x).__doc__.splitlines()[0]}""\n") def main(args): From 5f88c4aad98d92b722709c4ce2978170d32494f5 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 20 Nov 2019 19:31:30 +0300 Subject: [PATCH 238/319] Add example of usage for Aroon, Aroon Oscillator --- user_data/strategies/sample_strategy.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/user_data/strategies/sample_strategy.py b/user_data/strategies/sample_strategy.py index 36dea65c9..77a2d261a 100644 --- a/user_data/strategies/sample_strategy.py +++ b/user_data/strategies/sample_strategy.py @@ -107,10 +107,16 @@ class SampleStrategy(IStrategy): # RSI dataframe['rsi'] = ta.RSI(dataframe) - # ADX dataframe['adx'] = ta.ADX(dataframe) + """ + # Aroon, Aroon Oscillator + aroon = ta.AROON(dataframe) + dataframe['aroonup'] = aroon['aroonup'] + dataframe['aroondown'] = aroon['aroondown'] + dataframe['aroonosc'] = ta.AROONOSC(dataframe) + # Awesome oscillator dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) From 8b639b5026a2b7cd745de08cf764f7ddefcea9fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 20 Nov 2019 19:54:00 +0100 Subject: [PATCH 239/319] Remove only :return: --- scripts/rest_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 03e4fc76b..9e52de2bb 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -10,6 +10,7 @@ so it can be used as a standalone script. import argparse import inspect import json +import re import logging import sys from pathlib import Path @@ -238,7 +239,8 @@ def print_commands(): print("Possible commands:\n") for x, y in inspect.getmembers(client): if not x.startswith('_'): - print(f"{x}""\n "f"{getattr(client, x).__doc__.splitlines()[0]}""\n") + doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip() + print(f"{x}\n\t{doc}\n") def main(args): From 9aac080414c5d7b96870b1aebf8f15cbe5c232f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 20 Nov 2019 20:08:12 +0100 Subject: [PATCH 240/319] Fix 'remaining' bug when handling buy timeout --- freqtrade/freqtradebot.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 58f676aef..702e8483f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -787,7 +787,7 @@ class FreqtradeBot: continue # Check if trade is still actually open - if float(order['remaining']) == 0.0: + if float(order.get('remaining', 0.0)) == 0.0: self.wallets.update() continue @@ -813,7 +813,8 @@ class FreqtradeBot: }) def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool: - """Buy timeout - cancel order + """ + Buy timeout - cancel order :return: True if order was fully cancelled """ reason = "cancelled due to timeout" @@ -824,18 +825,21 @@ class FreqtradeBot: corder = order reason = "canceled on Exchange" - if corder['remaining'] == corder['amount']: + if corder.get('remaining', order['remaining']) == order['amount']: # if trade is not partially completed, just delete the trade self.handle_buy_order_full_cancel(trade, reason) return True # if trade is partially complete, edit the stake details for the trade # and close the order - trade.amount = corder['amount'] - corder['remaining'] + # cancel_order may not contain the full order dict, so we need to fallback + # to the order dict aquired before cancelling. + # we need to fall back to the values from order if corder does not contain these keys. + trade.amount = order['amount'] - corder.get('remaining', order['remaining']) trade.stake_amount = trade.amount * trade.open_rate # verify if fees were taken from amount to avoid problems during selling try: - new_amount = self.get_real_amount(trade, corder, trade.amount) + new_amount = self.get_real_amount(trade, order, trade.amount) if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): trade.amount = new_amount # Fee was applied, so set to 0 From a5bd4e329a9da98191e20b46e15fc496d55ec2c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 20 Nov 2019 20:33:05 +0100 Subject: [PATCH 241/319] improve cancel_order handling --- freqtrade/freqtradebot.py | 3 ++- tests/test_freqtradebot.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 702e8483f..358c63f90 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -839,7 +839,8 @@ class FreqtradeBot: trade.stake_amount = trade.amount * trade.open_rate # verify if fees were taken from amount to avoid problems during selling try: - new_amount = self.get_real_amount(trade, order, trade.amount) + new_amount = self.get_real_amount(trade, corder if 'fee' in corder else order, + trade.amount) if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): trade.amount = new_amount # Fee was applied, so set to 0 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f3baff7ce..c195ce39b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1804,7 +1804,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade, fee, mocker) -> None: rpc_mock = patch_RPCManager(mocker) - cancel_order_mock = MagicMock() + cancel_order_mock = MagicMock(return_value=limit_buy_order_old) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', From eac01960a79e2031630e90169e23259194275fa2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 20 Nov 2019 20:37:46 +0100 Subject: [PATCH 242/319] Add testcase for empty-order case --- tests/test_freqtradebot.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c195ce39b..b01c8e247 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2089,6 +2089,29 @@ def test_handle_timedout_limit_buy(mocker, default_conf, limit_buy_order) -> Non assert cancel_order_mock.call_count == 1 +def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_order) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + cancel_order_mock = MagicMock(return_value={}) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + cancel_order=cancel_order_mock + ) + + freqtrade = FreqtradeBot(default_conf) + + Trade.session = MagicMock() + trade = MagicMock() + limit_buy_order['remaining'] = limit_buy_order['amount'] + assert freqtrade.handle_timedout_limit_buy(trade, limit_buy_order) + assert cancel_order_mock.call_count == 1 + + cancel_order_mock.reset_mock() + limit_buy_order['amount'] = 2 + assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order) + assert cancel_order_mock.call_count == 1 + + def test_handle_timedout_limit_sell(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From b8aa727edf27f7b2d00d0fe2b7e4eeeb44b87943 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 21 Nov 2019 04:59:38 +0300 Subject: [PATCH 243/319] Fix second part of freqtrade-strategies #51 --- freqtrade/strategy/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d42f8e989..e208138e7 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -109,8 +109,8 @@ class IStrategy(ABC): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: DataProvider - wallets: Wallets + dp: Optional[DataProvider] = None + wallets: Optional[Wallets] = None def __init__(self, config: dict) -> None: self.config = config From 258d4bd6ae7ed19b4f682029c86d66974e2032f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 12:57:38 +0100 Subject: [PATCH 244/319] move sample-files from user_data to templates folder --- {user_data/hyperopts => freqtrade/templates}/sample_hyperopt.py | 0 .../hyperopts => freqtrade/templates}/sample_hyperopt_advanced.py | 0 .../hyperopts => freqtrade/templates}/sample_hyperopt_loss.py | 0 {user_data/strategies => freqtrade/templates}/sample_strategy.py | 0 user_data/hyperopts/__init__.py | 0 user_data/strategies/__init__.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {user_data/hyperopts => freqtrade/templates}/sample_hyperopt.py (100%) rename {user_data/hyperopts => freqtrade/templates}/sample_hyperopt_advanced.py (100%) rename {user_data/hyperopts => freqtrade/templates}/sample_hyperopt_loss.py (100%) rename {user_data/strategies => freqtrade/templates}/sample_strategy.py (100%) delete mode 100644 user_data/hyperopts/__init__.py delete mode 100644 user_data/strategies/__init__.py diff --git a/user_data/hyperopts/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py similarity index 100% rename from user_data/hyperopts/sample_hyperopt.py rename to freqtrade/templates/sample_hyperopt.py diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py similarity index 100% rename from user_data/hyperopts/sample_hyperopt_advanced.py rename to freqtrade/templates/sample_hyperopt_advanced.py diff --git a/user_data/hyperopts/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py similarity index 100% rename from user_data/hyperopts/sample_hyperopt_loss.py rename to freqtrade/templates/sample_hyperopt_loss.py diff --git a/user_data/strategies/sample_strategy.py b/freqtrade/templates/sample_strategy.py similarity index 100% rename from user_data/strategies/sample_strategy.py rename to freqtrade/templates/sample_strategy.py diff --git a/user_data/hyperopts/__init__.py b/user_data/hyperopts/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/user_data/strategies/__init__.py b/user_data/strategies/__init__.py deleted file mode 100644 index e69de29bb..000000000 From fd45ebd0e9d8ef49f00f2ccf3f8145debbdd2db8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 13:28:35 +0100 Subject: [PATCH 245/319] Copy templates when creating userdir --- .../configuration/directory_operations.py | 23 ++++++++++++- freqtrade/constants.py | 8 +++++ freqtrade/utils.py | 5 +-- tests/test_configuration.py | 34 ++++++++++++++++++- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 395accd90..e39c485f3 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -1,8 +1,10 @@ import logging -from typing import Any, Dict, Optional +import shutil from pathlib import Path +from typing import Any, Dict, Optional from freqtrade import OperationalException +from freqtrade.constants import USER_DATA_FILES logger = logging.getLogger(__name__) @@ -48,3 +50,22 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path: if not subfolder.is_dir(): subfolder.mkdir(parents=False) return folder + + +def copy_sample_files(directory: Path) -> None: + """ + Copy files from templates to User data directory. + :param directory: Directory to copy data to + """ + if not directory.is_dir(): + raise OperationalException(f"Directory `{directory}` does not exist.") + sourcedir = Path(__file__).parents[1] / "templates" + for source, target in USER_DATA_FILES.items(): + targetdir = directory / target + if not targetdir.is_dir(): + raise OperationalException(f"Directory `{targetdir}` does not exist.") + targetfile = targetdir / source + if targetfile.exists(): + logger.warning(f"File `{targetfile}` exists already, not deploying sample file.") + continue + shutil.copy(str(sourcedir / source), str(targetfile)) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a92371bc3..e28016eea 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -22,6 +22,14 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'P DRY_RUN_WALLET = 999.9 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons +# Soure files with destination directories +USER_DATA_FILES = { + 'sample_strategy.py': 'strategies', + 'sample_hyperopt_advanced.py': 'hyperopts', + 'sample_hyperopt_loss.py': 'hyperopts', + 'sample_hyperopt.py': 'hyperopts', +} + TIMEFRAMES = [ '1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', diff --git a/freqtrade/utils.py b/freqtrade/utils.py index b8ab7504e..c6422d04c 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -11,7 +11,7 @@ from tabulate import tabulate from freqtrade import OperationalException from freqtrade.configuration import Configuration, TimeRange, remove_credentials -from freqtrade.configuration.directory_operations import create_userdata_dir +from freqtrade.configuration.directory_operations import create_userdata_dir, copy_sample_files from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) @@ -81,7 +81,8 @@ def start_create_userdir(args: Dict[str, Any]) -> None: :return: None """ if "user_data_dir" in args and args["user_data_dir"]: - create_userdata_dir(args["user_data_dir"], create_dir=True) + userdir = create_userdata_dir(args["user_data_dir"], create_dir=True) + copy_sample_files(userdir) else: logger.warning("`create-userdir` requires --userdir to be set.") sys.exit(1) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 240b7c784..d74d64c95 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -18,7 +18,7 @@ from freqtrade.configuration.deprecated_settings import ( check_conflicting_settings, process_deprecated_setting, process_temporary_deprecated_settings) from freqtrade.configuration.directory_operations import (create_datadir, - create_userdata_dir) + create_userdata_dir, copy_sample_files) 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 @@ -709,6 +709,38 @@ def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> N assert md.call_count == 0 +def test_copy_sample_files(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + copymock = mocker.patch('shutil.copy', MagicMock()) + + copy_sample_files(Path('/tmp/bar')) + assert copymock.call_count == 4 + assert copymock.call_args_list[0][0][1] == '/tmp/bar/strategies/sample_strategy.py' + assert copymock.call_args_list[1][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_advanced.py' + assert copymock.call_args_list[2][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_loss.py' + assert copymock.call_args_list[3][0][1] == '/tmp/bar/hyperopts/sample_hyperopt.py' + + +def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + mocker.patch('shutil.copy', MagicMock()) + with pytest.raises(OperationalException, + match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist\."): + copy_sample_files(Path('/tmp/bar')) + + mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False])) + + with pytest.raises(OperationalException, + match=r"Directory `.{1,2}tmp.{1,2}bar.{1,2}strategies` does not exist\."): + copy_sample_files(Path('/tmp/bar')) + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + copy_sample_files(Path('/tmp/bar')) + assert log_has_re(r"File `.*` exists already, not deploying sample.*", caplog) + + def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 with pytest.raises(OperationalException, match='The config stoploss needs to be different ' From 1d2ef5c2ce8d1fd0ce9310aaa38dbaeba0ca6258 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 13:30:26 +0100 Subject: [PATCH 246/319] Extract directory_operation tests to it's own test file --- tests/test_configuration.py | 73 -------------------------- tests/test_directory_operations.py | 82 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 73 deletions(-) create mode 100644 tests/test_directory_operations.py diff --git a/tests/test_configuration.py b/tests/test_configuration.py index d74d64c95..e971d15ab 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -17,8 +17,6 @@ from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import ( check_conflicting_settings, process_deprecated_setting, process_temporary_deprecated_settings) -from freqtrade.configuration.directory_operations import (create_datadir, - create_userdata_dir, copy_sample_files) from freqtrade.configuration.load_config import load_config_file from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers @@ -670,77 +668,6 @@ def test_validate_default_conf(default_conf) -> None: validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) -def test_create_datadir(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - create_datadir(default_conf, '/foo/bar') - assert md.call_args[1]['parents'] is True - assert log_has('Created data directory: /foo/bar', caplog) - - -def test_create_userdata_dir(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - x = create_userdata_dir('/tmp/bar', create_dir=True) - assert md.call_count == 7 - assert md.call_args[1]['parents'] is False - assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog) - assert isinstance(x, Path) - assert str(x) == str(Path("/tmp/bar")) - - -def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - create_userdata_dir('/tmp/bar') - assert md.call_count == 0 - - -def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - with pytest.raises(OperationalException, - match=r'Directory `.{1,2}tmp.{1,2}bar` does not exist.*'): - create_userdata_dir('/tmp/bar', create_dir=False) - assert md.call_count == 0 - - -def test_copy_sample_files(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) - mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - copymock = mocker.patch('shutil.copy', MagicMock()) - - copy_sample_files(Path('/tmp/bar')) - assert copymock.call_count == 4 - assert copymock.call_args_list[0][0][1] == '/tmp/bar/strategies/sample_strategy.py' - assert copymock.call_args_list[1][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_advanced.py' - assert copymock.call_args_list[2][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_loss.py' - assert copymock.call_args_list[3][0][1] == '/tmp/bar/hyperopts/sample_hyperopt.py' - - -def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - mocker.patch('shutil.copy', MagicMock()) - with pytest.raises(OperationalException, - match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist\."): - copy_sample_files(Path('/tmp/bar')) - - mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False])) - - with pytest.raises(OperationalException, - match=r"Directory `.{1,2}tmp.{1,2}bar.{1,2}strategies` does not exist\."): - copy_sample_files(Path('/tmp/bar')) - mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) - mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - copy_sample_files(Path('/tmp/bar')) - assert log_has_re(r"File `.*` exists already, not deploying sample.*", caplog) - - def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 with pytest.raises(OperationalException, match='The config stoploss needs to be different ' diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py new file mode 100644 index 000000000..a7d98795b --- /dev/null +++ b/tests/test_directory_operations.py @@ -0,0 +1,82 @@ +# pragma pylint: disable=missing-docstring, protected-access, invalid-name +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from freqtrade import OperationalException +from freqtrade.configuration.directory_operations import (copy_sample_files, + create_datadir, + create_userdata_dir) +from tests.conftest import log_has, log_has_re + + +def test_create_datadir(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + create_datadir(default_conf, '/foo/bar') + assert md.call_args[1]['parents'] is True + assert log_has('Created data directory: /foo/bar', caplog) + + +def test_create_userdata_dir(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + x = create_userdata_dir('/tmp/bar', create_dir=True) + assert md.call_count == 7 + assert md.call_args[1]['parents'] is False + assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog) + assert isinstance(x, Path) + assert str(x) == str(Path("/tmp/bar")) + + +def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + create_userdata_dir('/tmp/bar') + assert md.call_count == 0 + + +def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + with pytest.raises(OperationalException, + match=r'Directory `.{1,2}tmp.{1,2}bar` does not exist.*'): + create_userdata_dir('/tmp/bar', create_dir=False) + assert md.call_count == 0 + + +def test_copy_sample_files(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + copymock = mocker.patch('shutil.copy', MagicMock()) + + copy_sample_files(Path('/tmp/bar')) + assert copymock.call_count == 4 + assert copymock.call_args_list[0][0][1] == '/tmp/bar/strategies/sample_strategy.py' + assert copymock.call_args_list[1][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_advanced.py' + assert copymock.call_args_list[2][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_loss.py' + assert copymock.call_args_list[3][0][1] == '/tmp/bar/hyperopts/sample_hyperopt.py' + + +def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + mocker.patch('shutil.copy', MagicMock()) + with pytest.raises(OperationalException, + match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist\."): + copy_sample_files(Path('/tmp/bar')) + + mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False])) + + with pytest.raises(OperationalException, + match=r"Directory `.{1,2}tmp.{1,2}bar.{1,2}strategies` does not exist\."): + copy_sample_files(Path('/tmp/bar')) + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + copy_sample_files(Path('/tmp/bar')) + assert log_has_re(r"File `.*` exists already, not deploying sample.*", caplog) From 084efc98d74cb4ab144b48d86be7f72423762fd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 13:41:25 +0100 Subject: [PATCH 247/319] Address test-failures due to file moves --- tests/optimize/test_backtesting.py | 1 + tests/strategy/test_strategy.py | 8 +++++--- tests/test_utils.py | 2 ++ user_data/hyperopts/.gitkeep | 0 user_data/strategies/.gitkeep | 0 5 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 user_data/hyperopts/.gitkeep create mode 100644 user_data/strategies/.gitkeep diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index b722dd3f8..e74ead33d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -869,6 +869,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'backtesting', '--config', 'config.json', '--datadir', str(testdatadir), + '--strategy-path', str(Path(__file__).parents[2] / 'freqtrade/templates'), '--ticker-interval', '1m', '--timerange', '1510694220-1510700340', '--enable-position-stacking', diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 97affc99c..2b84bc6ee 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -36,13 +36,15 @@ def test_search_strategy(): def test_load_strategy(default_conf, result): - default_conf.update({'strategy': 'SampleStrategy'}) + default_conf.update({'strategy': 'SampleStrategy', + 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') + }) resolver = StrategyResolver(default_conf) assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_base64(result, caplog, default_conf): - with open("user_data/strategies/sample_strategy.py", "rb") as file: + with (Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py').open("rb") as file: encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) @@ -57,7 +59,7 @@ def test_load_strategy_invalid_directory(result, caplog, default_conf): default_conf['strategy'] = 'SampleStrategy' resolver = StrategyResolver(default_conf) extra_dir = Path.cwd() / 'some/path' - resolver._load_strategy('SampleStrategy', config=default_conf, extra_dir=extra_dir) + resolver._load_strategy('DefaultStrategy', config=default_conf, extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) diff --git a/tests/test_utils.py b/tests/test_utils.py index bbb4fc648..88c9af35d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -442,6 +442,7 @@ def test_create_datadir_failed(caplog): def test_create_datadir(caplog, mocker): cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) + csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock()) args = [ "create-userdir", "--userdir", @@ -450,6 +451,7 @@ def test_create_datadir(caplog, mocker): start_create_userdir(get_args(args)) assert cud.call_count == 1 + assert csf.call_count == 1 assert len(caplog.record_tuples) == 0 diff --git a/user_data/hyperopts/.gitkeep b/user_data/hyperopts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/user_data/strategies/.gitkeep b/user_data/strategies/.gitkeep new file mode 100644 index 000000000..e69de29bb From 471bd4d889fa2343bdc028755356ce0f212e2da7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 13:52:59 +0100 Subject: [PATCH 248/319] Small stylistic fixes --- freqtrade/constants.py | 1 + freqtrade/templates/sample_hyperopt_advanced.py | 4 +--- .../templates}/strategy_analysis_example.ipynb | 0 tests/test_directory_operations.py | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) rename {user_data/notebooks => freqtrade/templates}/strategy_analysis_example.ipynb (100%) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e28016eea..96109bc94 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -28,6 +28,7 @@ USER_DATA_FILES = { 'sample_hyperopt_advanced.py': 'hyperopts', 'sample_hyperopt_loss.py': 'hyperopts', 'sample_hyperopt.py': 'hyperopts', + 'strategy_analysis_example.ipynb': 'notebooks', } TIMEFRAMES = [ diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 66182edcf..7ababc16c 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -1,11 +1,9 @@ # 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 # noqa F401 import talib.abstract as ta from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb similarity index 100% rename from user_data/notebooks/strategy_analysis_example.ipynb rename to freqtrade/templates/strategy_analysis_example.ipynb diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index a7d98795b..5c2485fc3 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -56,11 +56,12 @@ def test_copy_sample_files(mocker, default_conf, caplog) -> None: copymock = mocker.patch('shutil.copy', MagicMock()) copy_sample_files(Path('/tmp/bar')) - assert copymock.call_count == 4 + assert copymock.call_count == 5 assert copymock.call_args_list[0][0][1] == '/tmp/bar/strategies/sample_strategy.py' assert copymock.call_args_list[1][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_advanced.py' assert copymock.call_args_list[2][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_loss.py' assert copymock.call_args_list[3][0][1] == '/tmp/bar/hyperopts/sample_hyperopt.py' + assert copymock.call_args_list[4][0][1] == '/tmp/bar/notebooks/strategy_analysis_example.ipynb' def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: From 19b1a6c6381c9541ca0db2f11b06b90916c70f93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 13:55:26 +0100 Subject: [PATCH 249/319] create-userdir should create the notebooks folder, too --- freqtrade/configuration/directory_operations.py | 3 ++- tests/test_directory_operations.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index e39c485f3..8837c3572 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -33,7 +33,8 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path: :param create_dir: Create directory if it does not exist. :return: Path object containing the directory """ - sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "plot", "strategies", ] + sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "notebooks", + "plot", "strategies", ] folder = Path(directory) if not folder.is_dir(): if create_dir: diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index 5c2485fc3..064b5b6a3 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -25,7 +25,7 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None: md = mocker.patch.object(Path, 'mkdir', MagicMock()) x = create_userdata_dir('/tmp/bar', create_dir=True) - assert md.call_count == 7 + assert md.call_count == 8 assert md.call_args[1]['parents'] is False assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog) assert isinstance(x, Path) From 41494f28da2a678c8b685f5a462ffebcb180457f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 14:08:55 +0100 Subject: [PATCH 250/319] Allow resetting of the directory --- freqtrade/configuration/arguments.py | 2 +- freqtrade/configuration/cli_options.py | 5 +++++ freqtrade/configuration/directory_operations.py | 10 +++++++--- freqtrade/utils.py | 3 ++- tests/test_directory_operations.py | 5 ++++- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 29d0d98a2..149e28d2b 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -37,7 +37,7 @@ ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] -ARGS_CREATE_USERDIR = ["user_data_dir"] +ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 6dc5ef026..d7a496aa7 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -62,6 +62,11 @@ AVAILABLE_CLI_OPTIONS = { help='Path to userdata directory.', metavar='PATH', ), + "reset": Arg( + '--reset', + help='Reset sample files to their original state.', + action='store_true', + ), # Main options "strategy": Arg( '-s', '--strategy', diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 8837c3572..3dd76a025 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -53,10 +53,11 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path: return folder -def copy_sample_files(directory: Path) -> None: +def copy_sample_files(directory: Path, overwrite: bool = False) -> None: """ Copy files from templates to User data directory. :param directory: Directory to copy data to + :param overwrite: Overwrite existing sample files """ if not directory.is_dir(): raise OperationalException(f"Directory `{directory}` does not exist.") @@ -67,6 +68,9 @@ def copy_sample_files(directory: Path) -> None: raise OperationalException(f"Directory `{targetdir}` does not exist.") targetfile = targetdir / source if targetfile.exists(): - logger.warning(f"File `{targetfile}` exists already, not deploying sample file.") - continue + if not overwrite: + logger.warning(f"File `{targetfile}` exists already, not deploying sample file.") + continue + else: + logger.warning(f"File `{targetfile}` exists already, overwriting.") shutil.copy(str(sourcedir / source), str(targetfile)) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index c6422d04c..b9730da10 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,3 +1,4 @@ +from freqtrade.loggers import setup_logging import logging import sys from collections import OrderedDict @@ -82,7 +83,7 @@ def start_create_userdir(args: Dict[str, Any]) -> None: """ if "user_data_dir" in args and args["user_data_dir"]: userdir = create_userdata_dir(args["user_data_dir"], create_dir=True) - copy_sample_files(userdir) + copy_sample_files(userdir, overwrite=args["reset"]) else: logger.warning("`create-userdir` requires --userdir to be set.") sys.exit(1) diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index 064b5b6a3..c354b40b0 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -80,4 +80,7 @@ def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) mocker.patch.object(Path, "exists", MagicMock(return_value=True)) copy_sample_files(Path('/tmp/bar')) - assert log_has_re(r"File `.*` exists already, not deploying sample.*", caplog) + assert log_has_re(r"File `.*` exists already, not deploying sample file\.", caplog) + caplog.clear() + copy_sample_files(Path('/tmp/bar'), overwrite=True) + assert log_has_re(r"File `.*` exists already, overwriting\.", caplog) From ed1d4500996dd86516d29b7bf4e840610a71b5b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 15:02:42 +0100 Subject: [PATCH 251/319] Update documentation for create-userdir util --- .coveragerc | 1 + docs/utils.md | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/.coveragerc b/.coveragerc index 96ad6b09b..74dccbfe1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,7 @@ [run] omit = scripts/* + freqtrade/templates/* freqtrade/vendor/* freqtrade/__main__.py tests/* diff --git a/docs/utils.md b/docs/utils.md index 9f5792660..d9baee32c 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -2,6 +2,41 @@ Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section. +## Create userdir + +Creates the directory structure to hold your files for freqtrade. +Will also create strategy and hyperopt examples for you to get started. +Can be used multiple times - using `--reset` will reset the sample strategy and hyperopt files to their default state. + +``` +usage: freqtrade create-userdir [-h] [--userdir PATH] [--reset] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + --reset Reset sample files to their original state. +``` + +!!! Warning + Using `--reset` may result in loss of data, since this will overwrite all sample files without asking again. + +``` +├── backtest_results +├── data +├── hyperopt_results +├── hyperopts +│   ├── sample_hyperopt_advanced.py +│   ├── sample_hyperopt_loss.py +│   └── sample_hyperopt.py +├── notebooks +│   └── strategy_analysis_example.ipynb +├── plot +└── strategies + └── sample_strategy.py +``` + + ## List Exchanges Use the `list-exchanges` subcommand to see the exchanges available for the bot. From 8cf8ab089e8b962a4d2a68e293bc01c5c769f163 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 15:07:36 +0100 Subject: [PATCH 252/319] Add note about create-datadir to install instruction --- docs/developer.md | 4 ++-- docs/hyperopt.md | 6 +++--- docs/installation.md | 26 ++++++++++++++------------ docs/strategy-customization.md | 6 +++--- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index ab647726c..d731f1768 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -200,8 +200,8 @@ If the day shows the same day, then the last candle can be assumed as incomplete To keep the jupyter notebooks aligned with the documentation, the following should be ran after updating a example notebook. ``` bash -jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace user_data/notebooks/strategy_analysis_example.ipynb -jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md +jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace freqtrade/templates/strategy_analysis_example.ipynb +jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md ``` ## Continuous integration diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6c1505e75..8a750ef43 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -15,7 +15,7 @@ To learn how to get data for the pairs and exchange you're interrested in, head ## Prepare Hyperopting Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py). +the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. @@ -423,7 +423,7 @@ These ranges should be sufficient in most cases. The minutes in the steps (ROI d If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). ### Understand Hyperopt Stoploss results @@ -458,7 +458,7 @@ If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimiza If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). ### Validate backtesting results diff --git a/docs/installation.md b/docs/installation.md index 593da2acf..411441aa2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -162,7 +162,7 @@ Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git - +cd freqtrade ``` Optionally checkout the master branch to get the latest stable release: @@ -171,22 +171,24 @@ Optionally checkout the master branch to get the latest stable release: git checkout master ``` -#### 4. Initialize the configuration - -```bash -cd freqtrade -cp config.json.example config.json -``` - -> *To edit the config please refer to [Bot Configuration](configuration.md).* - -#### 5. Install python dependencies +#### 4. Install python dependencies ``` bash python3 -m pip install --upgrade pip python3 -m pip install -e . ``` +#### 5. Initialize the configuration + +```bash +# Initialize the user_directory +freqtrade create-userdir --userdir user_data/ + +cp config.json.example config.json +``` + +> *To edit the config please refer to [Bot Configuration](configuration.md).* + #### 6. Run the Bot If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. @@ -227,7 +229,7 @@ If that is not available on your system, feel free to try the instructions below Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. !!! Hint - Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document. + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. #### Clone the git repository diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 03aecd6ba..d0276579e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -48,7 +48,7 @@ Future versions will require this to be set. freqtrade trade --strategy AwesomeStrategy ``` -**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py) +**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py) file as reference.** !!! Note "Strategies and Backtesting" @@ -114,7 +114,7 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame ``` !!! Note "Want more indicator examples?" - Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). + Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). Then uncomment indicators you need. ### Strategy startup period @@ -478,7 +478,7 @@ Printing more than a few rows is also possible (simply use `print(dataframe)` i ### Where can i find a strategy template? The strategy template is located in the file -[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). +[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). ### Specify custom strategy location From e3cf6188a1d761963ad59cca4aa8f9c84833c68a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 16:04:44 +0100 Subject: [PATCH 253/319] Add first version of new-strategy generation from template --- freqtrade/configuration/arguments.py | 11 +- freqtrade/misc.py | 13 + freqtrade/templates/base_strategy.py.j2 | 306 ++++++++++++++++++++++++ freqtrade/utils.py | 23 +- 4 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 freqtrade/templates/base_strategy.py.j2 diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 149e28d2b..f48121032 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -39,6 +39,8 @@ ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] +ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy"] + ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase"] @@ -52,7 +54,7 @@ ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", "plot-dataframe", "plot-profit"] -NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"] +NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] class Arguments: @@ -117,6 +119,7 @@ class Arguments: from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.utils import (start_create_userdir, start_download_data, start_list_exchanges, start_list_markets, + start_new_strategy, start_list_timeframes, start_trading) from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit @@ -158,6 +161,12 @@ class Arguments: create_userdir_cmd.set_defaults(func=start_create_userdir) self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd) + # add new-strategy subcommand + build_strategy_cmd = subparsers.add_parser('new-strategy', + help="Create new strategy") + build_strategy_cmd.set_defaults(func=start_new_strategy) + self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd) + # Add list-exchanges subcommand list_exchanges_cmd = subparsers.add_parser( 'list-exchanges', diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 7682b5285..1745921d6 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -127,3 +127,16 @@ def round_dict(d, n): def plural(num, singular: str, plural: str = None) -> str: return singular if (num == 1 or num == -1) else plural or singular + 's' + + +def render_template(template: str, arguments: dict): + + from jinja2 import Environment, PackageLoader, select_autoescape + + env = Environment( + loader=PackageLoader('freqtrade', 'templates'), + autoescape=select_autoescape(['html', 'xml']) + ) + template = env.get_template(template) + + return template.render(**arguments) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 new file mode 100644 index 000000000..3fbd26997 --- /dev/null +++ b/freqtrade/templates/base_strategy.py.j2 @@ -0,0 +1,306 @@ + +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from pandas import DataFrame +# -------------------------------- + +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +import numpy # noqa + + +# This class is a sample. Feel free to customize it. +class {{ strategy }}(IStrategy): + """ + This is a strategy template to get you started.. + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + + You can: + :return: a Dataframe with all mandatory indicators for the strategies + - Rename the class name (Do not forget to update class_name) + - Add any methods you want to build your strategy + - Add any lib you need to build your strategy + + You must keep: + - the lib in the section "Do not remove these libs" + - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, + populate_sell_trend, hyperopt_space, buy_strategy_generator + """ + # Strategy interface version - allow new iterations of the strategy interface. + # Check the documentation or the Sample strategy to get the latest version. + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi". + minimal_roi = { + "60": 0.01, + "30": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy. + # This attribute will be overridden if the config file contains "stoploss". + stoploss = -0.10 + + # Trailing stoploss + trailing_stop = False + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured + + # Optimal ticker interval for the strategy. + ticker_interval = '5m' + + # Run "populate_indicators()" only for new candle. + process_only_new_candles = False + + # These values can be overridden in the "ask_strategy" section in the config. + use_sell_signal = True + sell_profit_only = False + ignore_roi_if_buy_signal = False + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional order type mapping. + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + # Optional order time in force. + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc' + } + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # Momentum Indicators + # ------------------------------------ + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + """ + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # Awesome oscillator + dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + dataframe['cci'] = ta.CCI(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # Minus Directional Indicator / Movement + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # ROC + dataframe['roc'] = ta.ROC(dataframe) + + # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + rsi = 0.1 * (dataframe['rsi'] - 50) + dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + + # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # Stoch + stoch = ta.STOCH(dataframe) + dataframe['slowd'] = stoch['slowd'] + dataframe['slowk'] = stoch['slowk'] + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Stoch RSI + stoch_rsi = ta.STOCHRSI(dataframe) + dataframe['fastd_rsi'] = stoch_rsi['fastd'] + dataframe['fastk_rsi'] = stoch_rsi['fastk'] + """ + + # Overlap Studies + # ------------------------------------ + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + """ + # EMA - Exponential Moving Average + dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) + + # SMA - Simple Moving Average + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + """ + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + """ + # Hammer: values [0, 100] + dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # Inverted Hammer: values [0, 100] + dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # Dragonfly Doji: values [0, 100] + dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # Piercing Line: values [0, 100] + dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # Morningstar: values [0, 100] + dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # Three White Soldiers: values [0, 100] + dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + """ + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + """ + # Hanging Man: values [0, 100] + dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # Shooting Star: values [0, 100] + dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # Gravestone Doji: values [0, 100] + dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # Dark Cloud Cover: values [0, 100] + dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # Evening Doji Star: values [0, 100] + dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # Evening Star: values [0, 100] + dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + """ + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + """ + # Three Line Strike: values [0, -100, 100] + dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # Spinning Top: values [0, -100, 100] + dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # Engulfing: values [0, -100, 100] + dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # Harami: values [0, -100, 100] + dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # Three Outside Up/Down: values [0, -100, 100] + dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # Three Inside Up/Down: values [0, -100, 100] + dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + """ + + # Chart type + # ------------------------------------ + """ + # Heikinashi stategy + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['ha_open'] = heikinashi['open'] + dataframe['ha_close'] = heikinashi['close'] + dataframe['ha_high'] = heikinashi['high'] + dataframe['ha_low'] = heikinashi['low'] + """ + + # Retrieve best bid and best ask from the orderbook + # ------------------------------------ + """ + # first check if dataprovider is available + if self.dp: + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] + """ + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle + (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 + (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle + (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'sell'] = 1 + return dataframe diff --git a/freqtrade/utils.py b/freqtrade/utils.py index b9730da10..974d5a2c3 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -18,7 +18,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_trades_data) from freqtrade.exchange import (available_exchanges, ccxt_exchanges, market_is_active, symbol_is_pair) -from freqtrade.misc import plural +from freqtrade.misc import plural, render_template from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -89,6 +89,27 @@ def start_create_userdir(args: Dict[str, Any]) -> None: sys.exit(1) +def start_new_strategy(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + if "strategy" in args and args["strategy"]: + new_path = config['user_data_dir'] / "strategies" / (args["strategy"] + ".py") + + if new_path.exists(): + raise OperationalException(f"`{new_path}` already exists. " + "Please choose another Strategy Name.") + + strategy_text = render_template(template='base_strategy.py.j2', + arguments={"strategy": args["strategy"]}) + + logger.info(f"Writing strategy to `{new_path}`.") + new_path.write_text(strategy_text) + else: + logger.warning("`new-strategy` requires --strategy to be set.") + sys.exit(1) + + def start_download_data(args: Dict[str, Any]) -> None: """ Download data (former download_backtest_data.py script) From 98baae9456c7e895e39c7fe794d2ceeabd5aecb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 16:09:30 +0100 Subject: [PATCH 254/319] Add jinja2 to requirements --- requirements-common.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-common.txt b/requirements-common.txt index 63cf48eee..2c176e9c3 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -12,6 +12,7 @@ jsonschema==3.1.1 TA-Lib==0.4.17 tabulate==0.8.6 coinmarketcap==5.0.3 +jinja2==2.10.3 # find first, C search in arrays py_find_1st==1.1.4 diff --git a/setup.py b/setup.py index 50b8eee9c..3710bcdc0 100644 --- a/setup.py +++ b/setup.py @@ -78,6 +78,7 @@ setup(name='freqtrade', 'python-rapidjson', 'sdnotify', 'colorama', + 'jinja2', # from requirements.txt 'numpy', 'pandas', From e492d47621fe68bb2d1aae5e9dcc1f5e541c605b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Nov 2019 19:50:42 +0100 Subject: [PATCH 255/319] Disallow usage of DefaultStrategy --- freqtrade/utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 974d5a2c3..2a6b0182c 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,4 +1,4 @@ -from freqtrade.loggers import setup_logging +import csv import logging import sys from collections import OrderedDict @@ -6,18 +6,20 @@ from pathlib import Path from typing import Any, Dict, List import arrow -import csv import rapidjson from tabulate import tabulate from freqtrade import OperationalException -from freqtrade.configuration import Configuration, TimeRange, remove_credentials -from freqtrade.configuration.directory_operations import create_userdata_dir, copy_sample_files +from freqtrade.configuration import (Configuration, TimeRange, + remove_credentials) +from freqtrade.configuration.directory_operations import (copy_sample_files, + create_userdata_dir) +from freqtrade.constants import DEFAULT_STRATEGY from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) -from freqtrade.exchange import (available_exchanges, ccxt_exchanges, market_is_active, - symbol_is_pair) +from freqtrade.exchange import (available_exchanges, ccxt_exchanges, + market_is_active, symbol_is_pair) from freqtrade.misc import plural, render_template from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -94,6 +96,9 @@ def start_new_strategy(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if "strategy" in args and args["strategy"]: + if args["strategy"] == DEFAULT_STRATEGY: + raise OperationalException("DefaultStrategy is not allowed as name.") + new_path = config['user_data_dir'] / "strategies" / (args["strategy"] + ".py") if new_path.exists(): From 8c2ff2f46e3d38a118bcfe57363cf3569c5618a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 10:42:17 +0100 Subject: [PATCH 256/319] Add template for new-hyperopt command --- freqtrade/configuration/arguments.py | 12 +- freqtrade/templates/base_hyperopt.py.j2 | 154 ++++++++++++++++++++++++ freqtrade/utils.py | 26 +++- 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 freqtrade/templates/base_hyperopt.py.j2 diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index f48121032..4ba2d04c4 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -41,6 +41,8 @@ ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy"] +ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt"] + ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase"] @@ -54,7 +56,7 @@ ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", "plot-dataframe", "plot-profit"] -NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] +NO_CONF_ALLOWED = ["create-userdir", "list-exchanges","new-hyperopt", "new-strategy"] class Arguments: @@ -119,7 +121,7 @@ class Arguments: from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.utils import (start_create_userdir, start_download_data, start_list_exchanges, start_list_markets, - start_new_strategy, + start_new_hyperopt, start_new_strategy, start_list_timeframes, start_trading) from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit @@ -167,6 +169,12 @@ class Arguments: build_strategy_cmd.set_defaults(func=start_new_strategy) self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd) + # add new-hyperopt subcommand + build_hyperopt_cmd = subparsers.add_parser('new-hyperopt', + help="Create new hyperopt") + build_hyperopt_cmd.set_defaults(func=start_new_hyperopt) + self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd) + # Add list-exchanges subcommand list_exchanges_cmd = subparsers.add_parser( 'list-exchanges', diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 new file mode 100644 index 000000000..ad30cfe55 --- /dev/null +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -0,0 +1,154 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from functools import reduce +from typing import Any, Callable, Dict, List + +import numpy as np # noqa +import talib.abstract as ta +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real # noqa + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +class {{ hyperopt }}(IHyperOpt): + """ + This is a Hyperopt template to get you started. + + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md + + You should: + - Add any lib you need to build your hyperopt. + + You must keep: + - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. + + The roi_space, generate_roi_table, stoploss_space methods are no longer required to be + copied in every custom hyperopt. However, you may override them if you need the + 'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade. + Sample implementation of these methods can be found in + https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py + """ + + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + 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 buy 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. + """ + 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') + ] diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 2a6b0182c..dd61cfcab 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -14,7 +14,7 @@ from freqtrade.configuration import (Configuration, TimeRange, remove_credentials) from freqtrade.configuration.directory_operations import (copy_sample_files, create_userdata_dir) -from freqtrade.constants import DEFAULT_STRATEGY +from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_STRATEGY from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) @@ -115,6 +115,30 @@ def start_new_strategy(args: Dict[str, Any]) -> None: sys.exit(1) +def start_new_hyperopt(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + if "hyperopt" in args and args["hyperopt"]: + if args["hyperopt"] == DEFAULT_HYPEROPT: + raise OperationalException("DefaultHyperOpt is not allowed as name.") + + new_path = config['user_data_dir'] / "hyperopts" / (args["hyperopt"] + ".py") + + if new_path.exists(): + raise OperationalException(f"`{new_path}` already exists. " + "Please choose another Strategy Name.") + + strategy_text = render_template(template='base_hyperopt.py.j2', + arguments={"hyperopt": args["hyperopt"]}) + + logger.info(f"Writing hyperopt to `{new_path}`.") + new_path.write_text(strategy_text) + else: + logger.warning("`new-hyperopt` requires --hyperopt to be set.") + sys.exit(1) + + def start_download_data(args: Dict[str, Any]) -> None: """ Download data (former download_backtest_data.py script) From 8a1d02e185119ff11fd5b5031ced8f331acab9b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Nov 2019 15:34:09 +0100 Subject: [PATCH 257/319] Update numpy imports in sample strategies --- freqtrade/templates/base_strategy.py.j2 | 6 +++--- freqtrade/templates/sample_strategy.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 3fbd26997..312aa0f27 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -2,15 +2,15 @@ # --- Do not remove these libs --- from freqtrade.strategy.interface import IStrategy from pandas import DataFrame +import pandas as pd # -------------------------------- # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib -import numpy # noqa +import numpy as np # noqa -# This class is a sample. Feel free to customize it. class {{ strategy }}(IStrategy): """ This is a strategy template to get you started.. @@ -140,7 +140,7 @@ class {{ strategy }}(IStrategy): # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) rsi = 0.1 * (dataframe['rsi'] - 50) - dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 77a2d261a..d62e6120c 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -7,7 +7,8 @@ from pandas import DataFrame # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib -import numpy # noqa +import pandas as pd # noqa +import numpy as np # noqa # This class is a sample. Feel free to customize it. @@ -147,7 +148,7 @@ class SampleStrategy(IStrategy): # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) rsi = 0.1 * (dataframe['rsi'] - 50) - dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) From b36a1d3260d66ee21032fe19ec9aaf1e1ff27e14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 13:31:07 +0100 Subject: [PATCH 258/319] test new_stratgy --- tests/test_utils.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 88c9af35d..902d56082 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,8 +9,8 @@ from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, start_download_data, start_list_exchanges, start_list_markets, start_list_timeframes, - start_trading) -from tests.conftest import get_args, log_has, patch_exchange + start_new_strategy, start_trading) +from tests.conftest import get_args, log_has, log_has_re, patch_exchange def test_setup_utils_configuration(): @@ -455,6 +455,32 @@ def test_create_datadir(caplog, mocker): assert len(caplog.record_tuples) == 0 +def test_start_new_strategy(mocker, caplog): + wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) + args = [ + "new-strategy", + "--strategy", + "CoolNewStrategy" + ] + start_new_strategy(get_args(args)) + + assert wt_mock.call_count == 1 + assert "CoolNewStrategy" in wt_mock.call_args_list[0][0][0] + assert log_has_re("Writing strategy to .*", caplog) + + + +def test_start_new_strategy_DefaultStrat(mocker, caplog): + args = [ + "new-strategy", + "--strategy", + "DefaultStrategy" + ] + with pytest.raises(OperationalException, + match=r"DefaultStrategy is not allowed as name\."): + start_new_strategy(get_args(args)) + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From 65489c894d9cd2dc9383b8cff57e75a2ecf8a2de Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Nov 2019 13:33:37 +0100 Subject: [PATCH 259/319] Add no-arg test --- freqtrade/configuration/arguments.py | 2 +- freqtrade/utils.py | 6 ++---- tests/test_utils.py | 10 +++++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 4ba2d04c4..5cc56a8bc 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -56,7 +56,7 @@ ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", "plot-dataframe", "plot-profit"] -NO_CONF_ALLOWED = ["create-userdir", "list-exchanges","new-hyperopt", "new-strategy"] +NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"] class Arguments: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index dd61cfcab..314ec4f36 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -111,8 +111,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None: logger.info(f"Writing strategy to `{new_path}`.") new_path.write_text(strategy_text) else: - logger.warning("`new-strategy` requires --strategy to be set.") - sys.exit(1) + raise OperationalException("`new-strategy` requires --strategy to be set.") def start_new_hyperopt(args: Dict[str, Any]) -> None: @@ -135,8 +134,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: logger.info(f"Writing hyperopt to `{new_path}`.") new_path.write_text(strategy_text) else: - logger.warning("`new-hyperopt` requires --hyperopt to be set.") - sys.exit(1) + raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") def start_download_data(args: Dict[str, Any]) -> None: diff --git a/tests/test_utils.py b/tests/test_utils.py index 902d56082..c64050f38 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -469,7 +469,6 @@ def test_start_new_strategy(mocker, caplog): assert log_has_re("Writing strategy to .*", caplog) - def test_start_new_strategy_DefaultStrat(mocker, caplog): args = [ "new-strategy", @@ -481,6 +480,15 @@ def test_start_new_strategy_DefaultStrat(mocker, caplog): start_new_strategy(get_args(args)) +def test_start_new_strategy_no_arg(mocker, caplog): + args = [ + "new-strategy", + ] + with pytest.raises(OperationalException, + match="`new-strategy` requires --strategy to be set."): + start_new_strategy(get_args(args)) + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From 79891671e99eec20dbcd172ddaa242edd92be72b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Nov 2019 06:49:58 +0100 Subject: [PATCH 260/319] Adapt after rebase --- freqtrade/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 314ec4f36..cdbbc8163 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -14,7 +14,6 @@ from freqtrade.configuration import (Configuration, TimeRange, remove_credentials) from freqtrade.configuration.directory_operations import (copy_sample_files, create_userdata_dir) -from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_STRATEGY from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) @@ -96,7 +95,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if "strategy" in args and args["strategy"]: - if args["strategy"] == DEFAULT_STRATEGY: + if args["strategy"] == "DefaultStrategy": raise OperationalException("DefaultStrategy is not allowed as name.") new_path = config['user_data_dir'] / "strategies" / (args["strategy"] + ".py") @@ -119,7 +118,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if "hyperopt" in args and args["hyperopt"]: - if args["hyperopt"] == DEFAULT_HYPEROPT: + if args["hyperopt"] == "DefaultHyperopt": raise OperationalException("DefaultHyperOpt is not allowed as name.") new_path = config['user_data_dir'] / "hyperopts" / (args["hyperopt"] + ".py") From 37f813943228d557aae04481a12d41a593d928b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Nov 2019 14:47:44 +0100 Subject: [PATCH 261/319] Small stylistic fixes --- freqtrade/templates/base_hyperopt.py.j2 | 10 +++++++--- freqtrade/templates/base_strategy.py.j2 | 20 ++++++++++---------- tests/strategy/test_strategy.py | 2 +- tests/test_utils.py | 2 ++ 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 index ad30cfe55..75aedbf86 100644 --- a/freqtrade/templates/base_hyperopt.py.j2 +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -3,14 +3,18 @@ from functools import reduce from typing import Any, Callable, Dict, List -import numpy as np # noqa -import talib.abstract as ta from pandas import DataFrame +import pandas as pd # noqa +import numpy as np # noqa from skopt.space import Categorical, Dimension, Integer, Real # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + class {{ hyperopt }}(IHyperOpt): """ diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 312aa0f27..46c118383 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -2,18 +2,18 @@ # --- Do not remove these libs --- from freqtrade.strategy.interface import IStrategy from pandas import DataFrame -import pandas as pd +import pandas as pd # noqa +import numpy as np # noqa # -------------------------------- # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib -import numpy as np # noqa class {{ strategy }}(IStrategy): """ - This is a strategy template to get you started.. + This is a strategy template to get you started. More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md You can: @@ -107,16 +107,15 @@ class {{ strategy }}(IStrategy): # RSI dataframe['rsi'] = ta.RSI(dataframe) - """ # ADX dataframe['adx'] = ta.ADX(dataframe) - + """ # Awesome oscillator dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) # Commodity Channel Index: values Oversold:<-100, Overbought:>100 dataframe['cci'] = ta.CCI(dataframe) - + """ # MACD macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] @@ -126,6 +125,7 @@ class {{ strategy }}(IStrategy): # MFI dataframe['mfi'] = ta.MFI(dataframe) + """ # Minus Directional Indicator / Movement dataframe['minus_dm'] = ta.MINUS_DM(dataframe) dataframe['minus_di'] = ta.MINUS_DI(dataframe) @@ -149,12 +149,13 @@ class {{ strategy }}(IStrategy): stoch = ta.STOCH(dataframe) dataframe['slowd'] = stoch['slowd'] dataframe['slowk'] = stoch['slowk'] - + """ # Stoch fast stoch_fast = ta.STOCHF(dataframe) dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastk'] = stoch_fast['fastk'] + """ # Stoch RSI stoch_rsi = ta.STOCHRSI(dataframe) dataframe['fastd_rsi'] = stoch_rsi['fastd'] @@ -178,12 +179,11 @@ class {{ strategy }}(IStrategy): dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) - # SAR Parabol - dataframe['sar'] = ta.SAR(dataframe) - # SMA - Simple Moving Average dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) """ + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) # TEMA - Triple Exponential Moving Average dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 2b84bc6ee..963d36c76 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -56,7 +56,7 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): - default_conf['strategy'] = 'SampleStrategy' + default_conf['strategy'] = 'DefaultStrategy' resolver = StrategyResolver(default_conf) extra_dir = Path.cwd() / 'some/path' resolver._load_strategy('DefaultStrategy', config=default_conf, extra_dir=extra_dir) diff --git a/tests/test_utils.py b/tests/test_utils.py index c64050f38..ce93c328d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -457,6 +457,8 @@ def test_create_datadir(caplog, mocker): def test_start_new_strategy(mocker, caplog): wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + args = [ "new-strategy", "--strategy", From 03cdfe8cae6812485073ce2d3ceff97d52593ef9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Nov 2019 15:52:32 +0100 Subject: [PATCH 262/319] Add tests for new-hyperopt --- freqtrade/utils.py | 2 +- tests/test_utils.py | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index cdbbc8163..7d3bd69ed 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -119,7 +119,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: if "hyperopt" in args and args["hyperopt"]: if args["hyperopt"] == "DefaultHyperopt": - raise OperationalException("DefaultHyperOpt is not allowed as name.") + raise OperationalException("DefaultHyperopt is not allowed as name.") new_path = config['user_data_dir'] / "hyperopts" / (args["hyperopt"] + ".py") diff --git a/tests/test_utils.py b/tests/test_utils.py index ce93c328d..1258c939c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,7 +9,8 @@ from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, start_download_data, start_list_exchanges, start_list_markets, start_list_timeframes, - start_new_strategy, start_trading) + start_new_hyperopt, start_new_strategy, + start_trading) from tests.conftest import get_args, log_has, log_has_re, patch_exchange @@ -491,6 +492,42 @@ def test_start_new_strategy_no_arg(mocker, caplog): start_new_strategy(get_args(args)) +def test_start_new_hyperopt(mocker, caplog): + wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + + args = [ + "new-hyperopt", + "--hyperopt", + "CoolNewhyperopt" + ] + start_new_hyperopt(get_args(args)) + + assert wt_mock.call_count == 1 + assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0] + assert log_has_re("Writing hyperopt to .*", caplog) + + +def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): + args = [ + "new-hyperopt", + "--hyperopt", + "DefaultHyperopt" + ] + with pytest.raises(OperationalException, + match=r"DefaultHyperopt is not allowed as name\."): + start_new_hyperopt(get_args(args)) + + +def test_start_new_hyperopt_no_arg(mocker, caplog): + args = [ + "new-hyperopt", + ] + with pytest.raises(OperationalException, + match="`new-hyperopt` requires --hyperopt to be set."): + start_new_hyperopt(get_args(args)) + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From cbb187e9b989a001f844d4fecf795f7084ce8e19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Nov 2019 22:00:50 +0100 Subject: [PATCH 263/319] Use constant for Strategy and hyperopt userdirpaths --- freqtrade/constants.py | 13 ++++++++----- freqtrade/misc.py | 4 ++-- freqtrade/resolvers/hyperopt_resolver.py | 6 +++--- freqtrade/resolvers/strategy_resolver.py | 3 ++- freqtrade/templates/sample_hyperopt.py | 2 +- freqtrade/utils.py | 9 +++++---- 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 96109bc94..bf5d822c6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -22,12 +22,15 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'P DRY_RUN_WALLET = 999.9 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons -# Soure files with destination directories +USERPATH_HYPEROPTS = 'hyperopts' +USERPATH_STRATEGY = 'strategies' + +# Soure files with destination directories within user-directory USER_DATA_FILES = { - 'sample_strategy.py': 'strategies', - 'sample_hyperopt_advanced.py': 'hyperopts', - 'sample_hyperopt_loss.py': 'hyperopts', - 'sample_hyperopt.py': 'hyperopts', + 'sample_strategy.py': USERPATH_STRATEGY, + 'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS, + 'sample_hyperopt_loss.py': USERPATH_HYPEROPTS, + 'sample_hyperopt.py': USERPATH_HYPEROPTS, 'strategy_analysis_example.ipynb': 'notebooks', } diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 1745921d6..6497a4727 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -129,7 +129,7 @@ def plural(num, singular: str, plural: str = None) -> str: return singular if (num == 1 or num == -1) else plural or singular + 's' -def render_template(template: str, arguments: dict): +def render_template(templatefile: str, arguments: dict): from jinja2 import Environment, PackageLoader, select_autoescape @@ -137,6 +137,6 @@ def render_template(template: str, arguments: dict): loader=PackageLoader('freqtrade', 'templates'), autoescape=select_autoescape(['html', 'xml']) ) - template = env.get_template(template) + template = env.get_template(templatefile) return template.render(**arguments) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index df1ff182c..05efa1164 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Optional, Dict from freqtrade import OperationalException -from freqtrade.constants import DEFAULT_HYPEROPT_LOSS +from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver @@ -58,7 +58,7 @@ class HyperOptResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='hyperopts', extra_dir=extra_dir) + user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, object_name=hyperopt_name, kwargs={'config': config}) @@ -110,7 +110,7 @@ class HyperOptLossResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='hyperopts', extra_dir=extra_dir) + user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, object_name=hyper_loss_name) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 102816981..9a76b9b74 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -129,7 +129,8 @@ class StrategyResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='strategies', extra_dir=extra_dir) + user_subdir=constants.USERPATH_STRATEGY, + extra_dir=extra_dir) if ":" in strategy_name: logger.info("loading base64 encoded strategy") diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index 3be05f121..77afb2b98 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -4,7 +4,7 @@ from functools import reduce from typing import Any, Callable, Dict, List import numpy as np # noqa -import talib.abstract as ta +import talib.abstract as ta # noqa from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 7d3bd69ed..3b37c6895 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -14,6 +14,7 @@ from freqtrade.configuration import (Configuration, TimeRange, remove_credentials) from freqtrade.configuration.directory_operations import (copy_sample_files, create_userdata_dir) +from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) @@ -98,13 +99,13 @@ def start_new_strategy(args: Dict[str, Any]) -> None: if args["strategy"] == "DefaultStrategy": raise OperationalException("DefaultStrategy is not allowed as name.") - new_path = config['user_data_dir'] / "strategies" / (args["strategy"] + ".py") + new_path = config['user_data_dir'] / USERPATH_STRATEGY / (args["strategy"] + ".py") if new_path.exists(): raise OperationalException(f"`{new_path}` already exists. " "Please choose another Strategy Name.") - strategy_text = render_template(template='base_strategy.py.j2', + strategy_text = render_template(templatefile='base_strategy.py.j2', arguments={"strategy": args["strategy"]}) logger.info(f"Writing strategy to `{new_path}`.") @@ -121,13 +122,13 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: if args["hyperopt"] == "DefaultHyperopt": raise OperationalException("DefaultHyperopt is not allowed as name.") - new_path = config['user_data_dir'] / "hyperopts" / (args["hyperopt"] + ".py") + new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args["hyperopt"] + ".py") if new_path.exists(): raise OperationalException(f"`{new_path}` already exists. " "Please choose another Strategy Name.") - strategy_text = render_template(template='base_hyperopt.py.j2', + strategy_text = render_template(templatefile='base_hyperopt.py.j2', arguments={"hyperopt": args["hyperopt"]}) logger.info(f"Writing hyperopt to `{new_path}`.") From ed04f7f39d2d41f19a40f9b378d8b5350c70b053 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 09:57:06 +0100 Subject: [PATCH 264/319] Create userdir and backtest SampleStrategy --- .github/workflows/ci.yml | 8 ++++++-- .travis.yml | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8932cf07..2e4ca87f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,11 +78,13 @@ jobs: - name: Backtesting run: | cp config.json.example config.json - freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy + freqtrade create-userdir --userdir user_data + freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - name: Hyperopt run: | cp config.json.example config.json + freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt - name: Flake8 @@ -139,11 +141,13 @@ jobs: - name: Backtesting run: | cp config.json.example config.json - freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy + freqtrade create-userdir --userdir user_data + freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - name: Hyperopt run: | cp config.json.example config.json + freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt - name: Flake8 diff --git a/.travis.yml b/.travis.yml index 6073e1cce..39e914fa4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,10 +28,12 @@ jobs: name: pytest - script: - cp config.json.example config.json - - freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy + freqtrade create-userdir --userdir user_data + - freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy name: backtest - script: - cp config.json.example config.json + - freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt name: hyperopt - script: flake8 From 671b98ecad17ed952c038dbc2d6b43de6d8f2514 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 10:11:58 +0100 Subject: [PATCH 265/319] Fix windows test --- tests/test_directory_operations.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index c354b40b0..db41e2da2 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -57,11 +57,16 @@ def test_copy_sample_files(mocker, default_conf, caplog) -> None: copy_sample_files(Path('/tmp/bar')) assert copymock.call_count == 5 - assert copymock.call_args_list[0][0][1] == '/tmp/bar/strategies/sample_strategy.py' - assert copymock.call_args_list[1][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_advanced.py' - assert copymock.call_args_list[2][0][1] == '/tmp/bar/hyperopts/sample_hyperopt_loss.py' - assert copymock.call_args_list[3][0][1] == '/tmp/bar/hyperopts/sample_hyperopt.py' - assert copymock.call_args_list[4][0][1] == '/tmp/bar/notebooks/strategy_analysis_example.ipynb' + assert copymock.call_args_list[0][0][1] == str( + Path('/tmp/bar') / 'strategies/sample_strategy.py') + assert copymock.call_args_list[1][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt_advanced.py') + assert copymock.call_args_list[2][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt_loss.py') + assert copymock.call_args_list[3][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt.py') + assert copymock.call_args_list[4][0][1] == str( + Path('/tmp/bar') / 'notebooks/strategy_analysis_example.ipynb') def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: From f7322358cf045621ee5cdde2b712af0248606503 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Nov 2019 10:31:21 +0100 Subject: [PATCH 266/319] Update documentation --- .travis.yml | 2 +- docs/hyperopt.md | 3 ++ docs/strategy-customization.md | 22 +++++++----- docs/utils.md | 64 ++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 39e914fa4..ec688a1f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ jobs: name: pytest - script: - cp config.json.example config.json - freqtrade create-userdir --userdir user_data + - freqtrade create-userdir --userdir user_data - freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy name: backtest - script: diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 8a750ef43..5a3ae7e3a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -19,6 +19,9 @@ the sample hyperopt file located in [user_data/hyperopts/](https://github.com/fr Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. +The simplest way to get started is to use `freqtrade new-hyperopt --hyperopt AwesomeHyperopt`. +This will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + ### Checklist on all tasks / possibilities in hyperopt Depending on the space you want to optimize, only some of the below are required: diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index d0276579e..352389d5e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -7,24 +7,28 @@ indicators. This is very simple. Copy paste your strategy file into the directory `user_data/strategies`. -Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: +Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`: -1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py` +1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py` 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) ```bash freqtrade trade --strategy AwesomeStrategy ``` -## Change your strategy +## Develop your own strategy -The bot includes a default strategy file. However, we recommend you to -use your own file to not have to lose your parameters every time the default -strategy file will be updated on Github. Put your custom strategy file -into the directory `user_data/strategies`. +The bot includes a default strategy file. +Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). -Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. -`cp user_data/strategies/sample_strategy.py user_data/strategies/awesome-strategy.py` +You will however most likely have your own idea for a strategy. +This Document intends to help you develop one for yourself. + +To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`. +This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`. + +!!! Note + This is just a template file, which will most likely not be profitable out of the box. ### Anatomy of a strategy diff --git a/docs/utils.md b/docs/utils.md index d9baee32c..26d354206 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -36,6 +36,70 @@ optional arguments: └── sample_strategy.py ``` +## Create new strategy + +Creates a new strategy from a template similar to SampleStrategy. +The file will be named inline with your class name, and will not overwrite existing files. + +Results will be located in `user_data/strategies/.py`. + +### Sample usage of new-strategy + +```bash +freqtrade new-strategy --strategy AwesomeStrategy +``` + +With custom user directory + +```bash +freqtrade new-strategy --userdir ~/.freqtrade/ --strategy AwesomeStrategy +``` + +### new-strategy complete options + +``` output +usage: freqtrade new-strategy [-h] [--userdir PATH] [-s NAME] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. +``` + +## Create new hyperopt + +Creates a new hyperopt from a template similar to SampleHyperopt. +The file will be named inline with your class name, and will not overwrite existing files. + +Results will be located in `user_data/hyperopts/.py`. + +### Sample usage of new-hyperopt + +```bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +With custom user directory + +```bash +freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt +``` + +### new-hyperopt complete options + +``` output +usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + --hyperopt NAME Specify hyperopt class name which will be used by the + bot. +``` ## List Exchanges From be4a4180ae9fcaca3d95db72d561c98d0bcd954d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Nov 2019 06:40:30 +0100 Subject: [PATCH 267/319] Use single line comments for samples --- freqtrade/templates/base_strategy.py.j2 | 186 +++++++++++------------ freqtrade/templates/sample_strategy.py | 189 +++++++++++------------- 2 files changed, 178 insertions(+), 197 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 46c118383..174c801ee 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -109,58 +109,61 @@ class {{ strategy }}(IStrategy): # ADX dataframe['adx'] = ta.ADX(dataframe) - """ - # Awesome oscillator - dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) - # Commodity Channel Index: values Oversold:<-100, Overbought:>100 - dataframe['cci'] = ta.CCI(dataframe) - """ + # # Aroon, Aroon Oscillator + # aroon = ta.AROON(dataframe) + # dataframe['aroonup'] = aroon['aroonup'] + # dataframe['aroondown'] = aroon['aroondown'] + # dataframe['aroonosc'] = ta.AROONOSC(dataframe) + + # # Awesome oscillator + # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + # dataframe['cci'] = ta.CCI(dataframe) + # MACD macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdhist'] = macd['macdhist'] - # MFI - dataframe['mfi'] = ta.MFI(dataframe) + # # MFI + # dataframe['mfi'] = ta.MFI(dataframe) - """ - # Minus Directional Indicator / Movement - dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # # Minus Directional Indicator / Movement + # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # Plus Directional Indicator / Movement - dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # # Plus Directional Indicator / Movement + # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + # dataframe['plus_di'] = ta.PLUS_DI(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # ROC - dataframe['roc'] = ta.ROC(dataframe) + # # ROC + # dataframe['roc'] = ta.ROC(dataframe) - # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) - rsi = 0.1 * (dataframe['rsi'] - 50) - dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + # # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + # rsi = 0.1 * (dataframe['rsi'] - 50) + # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) - # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) - dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + # # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # # Stoch + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] - # Stoch - stoch = ta.STOCH(dataframe) - dataframe['slowd'] = stoch['slowd'] - dataframe['slowk'] = stoch['slowk'] - """ # Stoch fast stoch_fast = ta.STOCHF(dataframe) dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastk'] = stoch_fast['fastk'] - """ - # Stoch RSI - stoch_rsi = ta.STOCHRSI(dataframe) - dataframe['fastd_rsi'] = stoch_rsi['fastd'] - dataframe['fastk_rsi'] = stoch_rsi['fastk'] - """ + # # Stoch RSI + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] # Overlap Studies # ------------------------------------ @@ -171,17 +174,16 @@ class {{ strategy }}(IStrategy): dataframe['bb_middleband'] = bollinger['mid'] dataframe['bb_upperband'] = bollinger['upper'] - """ - # EMA - Exponential Moving Average - dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + # # EMA - Exponential Moving Average + # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # # SMA - Simple Moving Average + # dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - # SMA - Simple Moving Average - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - """ # SAR Parabol dataframe['sar'] = ta.SAR(dataframe) @@ -197,65 +199,57 @@ class {{ strategy }}(IStrategy): # Pattern Recognition - Bullish candlestick patterns # ------------------------------------ - """ - # Hammer: values [0, 100] - dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - # Inverted Hammer: values [0, 100] - dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - # Dragonfly Doji: values [0, 100] - dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - # Piercing Line: values [0, 100] - dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - # Morningstar: values [0, 100] - dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - # Three White Soldiers: values [0, 100] - dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - """ + # # Hammer: values [0, 100] + # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # # Inverted Hammer: values [0, 100] + # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # # Dragonfly Doji: values [0, 100] + # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # # Piercing Line: values [0, 100] + # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # # Morningstar: values [0, 100] + # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # # Three White Soldiers: values [0, 100] + # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] # Pattern Recognition - Bearish candlestick patterns # ------------------------------------ - """ - # Hanging Man: values [0, 100] - dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - # Shooting Star: values [0, 100] - dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - # Gravestone Doji: values [0, 100] - dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - # Dark Cloud Cover: values [0, 100] - dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - # Evening Doji Star: values [0, 100] - dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - # Evening Star: values [0, 100] - dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - """ + # # Hanging Man: values [0, 100] + # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # # Shooting Star: values [0, 100] + # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # # Gravestone Doji: values [0, 100] + # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # # Dark Cloud Cover: values [0, 100] + # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # # Evening Doji Star: values [0, 100] + # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # # Evening Star: values [0, 100] + # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) # Pattern Recognition - Bullish/Bearish candlestick patterns # ------------------------------------ - """ - # Three Line Strike: values [0, -100, 100] - dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - # Spinning Top: values [0, -100, 100] - dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - # Engulfing: values [0, -100, 100] - dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - # Harami: values [0, -100, 100] - dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - # Three Outside Up/Down: values [0, -100, 100] - dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - # Three Inside Up/Down: values [0, -100, 100] - dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - """ + # # Three Line Strike: values [0, -100, 100] + # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # # Spinning Top: values [0, -100, 100] + # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # # Engulfing: values [0, -100, 100] + # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # # Harami: values [0, -100, 100] + # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # # Three Outside Up/Down: values [0, -100, 100] + # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # # Three Inside Up/Down: values [0, -100, 100] + # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - # Chart type - # ------------------------------------ - """ - # Heikinashi stategy - heikinashi = qtpylib.heikinashi(dataframe) - dataframe['ha_open'] = heikinashi['open'] - dataframe['ha_close'] = heikinashi['close'] - dataframe['ha_high'] = heikinashi['high'] - dataframe['ha_low'] = heikinashi['low'] - """ + # # Chart type + # # ------------------------------------ + # # Heikinashi stategy + # heikinashi = qtpylib.heikinashi(dataframe) + # dataframe['ha_open'] = heikinashi['open'] + # dataframe['ha_close'] = heikinashi['close'] + # dataframe['ha_high'] = heikinashi['high'] + # dataframe['ha_low'] = heikinashi['low'] # Retrieve best bid and best ask from the orderbook # ------------------------------------ diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index d62e6120c..724e52156 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -111,64 +111,60 @@ class SampleStrategy(IStrategy): # ADX dataframe['adx'] = ta.ADX(dataframe) - """ - # Aroon, Aroon Oscillator - aroon = ta.AROON(dataframe) - dataframe['aroonup'] = aroon['aroonup'] - dataframe['aroondown'] = aroon['aroondown'] - dataframe['aroonosc'] = ta.AROONOSC(dataframe) + # # Aroon, Aroon Oscillator + # aroon = ta.AROON(dataframe) + # dataframe['aroonup'] = aroon['aroonup'] + # dataframe['aroondown'] = aroon['aroondown'] + # dataframe['aroonosc'] = ta.AROONOSC(dataframe) - # Awesome oscillator - dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + # # Awesome oscillator + # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + # dataframe['cci'] = ta.CCI(dataframe) - # Commodity Channel Index: values Oversold:<-100, Overbought:>100 - dataframe['cci'] = ta.CCI(dataframe) - """ # MACD macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdhist'] = macd['macdhist'] - # MFI - dataframe['mfi'] = ta.MFI(dataframe) + # # MFI + # dataframe['mfi'] = ta.MFI(dataframe) - """ - # Minus Directional Indicator / Movement - dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # # Minus Directional Indicator / Movement + # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # Plus Directional Indicator / Movement - dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # # Plus Directional Indicator / Movement + # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + # dataframe['plus_di'] = ta.PLUS_DI(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # ROC - dataframe['roc'] = ta.ROC(dataframe) + # # ROC + # dataframe['roc'] = ta.ROC(dataframe) - # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) - rsi = 0.1 * (dataframe['rsi'] - 50) - dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + # # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + # rsi = 0.1 * (dataframe['rsi'] - 50) + # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) - # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) - dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + # # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # # Stoch + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] - # Stoch - stoch = ta.STOCH(dataframe) - dataframe['slowd'] = stoch['slowd'] - dataframe['slowk'] = stoch['slowk'] - """ # Stoch fast stoch_fast = ta.STOCHF(dataframe) dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastk'] = stoch_fast['fastk'] - """ - # Stoch RSI - stoch_rsi = ta.STOCHRSI(dataframe) - dataframe['fastd_rsi'] = stoch_rsi['fastd'] - dataframe['fastk_rsi'] = stoch_rsi['fastk'] - """ + # # Stoch RSI + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] # Overlap Studies # ------------------------------------ @@ -179,17 +175,16 @@ class SampleStrategy(IStrategy): dataframe['bb_middleband'] = bollinger['mid'] dataframe['bb_upperband'] = bollinger['upper'] - """ - # EMA - Exponential Moving Average - dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + # # EMA - Exponential Moving Average + # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # # SMA - Simple Moving Average + # dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - # SMA - Simple Moving Average - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - """ # SAR Parabol dataframe['sar'] = ta.SAR(dataframe) @@ -205,65 +200,57 @@ class SampleStrategy(IStrategy): # Pattern Recognition - Bullish candlestick patterns # ------------------------------------ - """ - # Hammer: values [0, 100] - dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - # Inverted Hammer: values [0, 100] - dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - # Dragonfly Doji: values [0, 100] - dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - # Piercing Line: values [0, 100] - dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - # Morningstar: values [0, 100] - dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - # Three White Soldiers: values [0, 100] - dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - """ + # # Hammer: values [0, 100] + # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # # Inverted Hammer: values [0, 100] + # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # # Dragonfly Doji: values [0, 100] + # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # # Piercing Line: values [0, 100] + # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # # Morningstar: values [0, 100] + # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # # Three White Soldiers: values [0, 100] + # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] # Pattern Recognition - Bearish candlestick patterns # ------------------------------------ - """ - # Hanging Man: values [0, 100] - dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - # Shooting Star: values [0, 100] - dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - # Gravestone Doji: values [0, 100] - dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - # Dark Cloud Cover: values [0, 100] - dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - # Evening Doji Star: values [0, 100] - dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - # Evening Star: values [0, 100] - dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - """ + # # Hanging Man: values [0, 100] + # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # # Shooting Star: values [0, 100] + # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # # Gravestone Doji: values [0, 100] + # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # # Dark Cloud Cover: values [0, 100] + # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # # Evening Doji Star: values [0, 100] + # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # # Evening Star: values [0, 100] + # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) # Pattern Recognition - Bullish/Bearish candlestick patterns # ------------------------------------ - """ - # Three Line Strike: values [0, -100, 100] - dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - # Spinning Top: values [0, -100, 100] - dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - # Engulfing: values [0, -100, 100] - dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - # Harami: values [0, -100, 100] - dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - # Three Outside Up/Down: values [0, -100, 100] - dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - # Three Inside Up/Down: values [0, -100, 100] - dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - """ + # # Three Line Strike: values [0, -100, 100] + # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # # Spinning Top: values [0, -100, 100] + # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # # Engulfing: values [0, -100, 100] + # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # # Harami: values [0, -100, 100] + # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # # Three Outside Up/Down: values [0, -100, 100] + # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # # Three Inside Up/Down: values [0, -100, 100] + # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - # Chart type - # ------------------------------------ - """ - # Heikinashi stategy - heikinashi = qtpylib.heikinashi(dataframe) - dataframe['ha_open'] = heikinashi['open'] - dataframe['ha_close'] = heikinashi['close'] - dataframe['ha_high'] = heikinashi['high'] - dataframe['ha_low'] = heikinashi['low'] - """ + # # Chart type + # # ------------------------------------ + # # Heikinashi stategy + # heikinashi = qtpylib.heikinashi(dataframe) + # dataframe['ha_open'] = heikinashi['open'] + # dataframe['ha_close'] = heikinashi['close'] + # dataframe['ha_high'] = heikinashi['high'] + # dataframe['ha_low'] = heikinashi['low'] # Retrieve best bid and best ask from the orderbook # ------------------------------------ From 5e5ef21f61f33a50c54b151cdf9081983394bc1b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Nov 2019 06:49:16 +0100 Subject: [PATCH 268/319] Align example imports --- freqtrade/templates/base_hyperopt.py.j2 | 7 ++++--- freqtrade/templates/base_strategy.py.j2 | 10 ++++++---- freqtrade/templates/sample_hyperopt.py | 9 +++++++-- freqtrade/templates/sample_hyperopt_advanced.py | 13 +++++++++---- freqtrade/templates/sample_strategy.py | 10 ++++++---- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 index 75aedbf86..c0f4e3292 100644 --- a/freqtrade/templates/base_hyperopt.py.j2 +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -1,18 +1,19 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# --- Do not remove these libs --- from functools import reduce from typing import Any, Callable, Dict, List -from pandas import DataFrame -import pandas as pd # noqa import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa from freqtrade.optimize.hyperopt_interface import IHyperOpt # -------------------------------- # Add your lib to import here -import talib.abstract as ta +import talib.abstract as ta # noqa import freqtrade.vendor.qtpylib.indicators as qtpylib diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 174c801ee..ccb8949e9 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -1,11 +1,13 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # --- Do not remove these libs --- -from freqtrade.strategy.interface import IStrategy -from pandas import DataFrame -import pandas as pd # noqa import numpy as np # noqa -# -------------------------------- +import pandas as pd # noqa +from pandas import DataFrame +from freqtrade.strategy.interface import IStrategy + +# -------------------------------- # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index 77afb2b98..f1dcb404a 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -1,16 +1,21 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# --- Do not remove these libs --- from functools import reduce from typing import Any, Callable, Dict, List import numpy as np # noqa -import talib.abstract as ta # noqa +import pandas as pd # noqa from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib + class SampleHyperOpt(IHyperOpt): """ diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 7ababc16c..5634c21ea 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -1,16 +1,21 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# --- Do not remove these libs --- from functools import reduce from typing import Any, Callable, Dict, List -import numpy as np # noqa F401 -import talib.abstract as ta +import numpy as np # noqa +import pandas as pd # noqa from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer, Real # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib + class AdvancedSampleHyperOpt(IHyperOpt): """ diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 724e52156..38a45c1f2 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -1,14 +1,16 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # --- Do not remove these libs --- -from freqtrade.strategy.interface import IStrategy +import numpy as np # noqa +import pandas as pd # noqa from pandas import DataFrame -# -------------------------------- +from freqtrade.strategy.interface import IStrategy + +# -------------------------------- # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib -import pandas as pd # noqa -import numpy as np # noqa # This class is a sample. Feel free to customize it. From b3dbb818388da57ee1bc6611e0b25dfdbec36367 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Nov 2019 07:13:56 +0100 Subject: [PATCH 269/319] Add subtemplates --- freqtrade/templates/base_strategy.py.j2 | 170 +----------------- .../templates/subtemplates/buy_trend_full.j2 | 3 + .../templates/subtemplates/indicators_full.j2 | 161 +++++++++++++++++ .../templates/subtemplates/sell_trend_full.j2 | 3 + freqtrade/utils.py | 3 +- 5 files changed, 172 insertions(+), 168 deletions(-) create mode 100644 freqtrade/templates/subtemplates/buy_trend_full.j2 create mode 100644 freqtrade/templates/subtemplates/indicators_full.j2 create mode 100644 freqtrade/templates/subtemplates/sell_trend_full.j2 diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index ccb8949e9..4c5fe9a0b 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -102,167 +102,7 @@ class {{ strategy }}(IStrategy): :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ - - # Momentum Indicators - # ------------------------------------ - - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # # Aroon, Aroon Oscillator - # aroon = ta.AROON(dataframe) - # dataframe['aroonup'] = aroon['aroonup'] - # dataframe['aroondown'] = aroon['aroondown'] - # dataframe['aroonosc'] = ta.AROONOSC(dataframe) - - # # Awesome oscillator - # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) - - # # Commodity Channel Index: values Oversold:<-100, Overbought:>100 - # dataframe['cci'] = ta.CCI(dataframe) - - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # # MFI - # dataframe['mfi'] = ta.MFI(dataframe) - - # # Minus Directional Indicator / Movement - # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # # Plus Directional Indicator / Movement - # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - # dataframe['plus_di'] = ta.PLUS_DI(dataframe) - # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # # ROC - # dataframe['roc'] = ta.ROC(dataframe) - - # # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) - # rsi = 0.1 * (dataframe['rsi'] - 50) - # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) - - # # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) - # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) - - # # Stoch - # stoch = ta.STOCH(dataframe) - # dataframe['slowd'] = stoch['slowd'] - # dataframe['slowk'] = stoch['slowk'] - - # Stoch fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # # Stoch RSI - # stoch_rsi = ta.STOCHRSI(dataframe) - # dataframe['fastd_rsi'] = stoch_rsi['fastd'] - # dataframe['fastk_rsi'] = stoch_rsi['fastk'] - - # Overlap Studies - # ------------------------------------ - - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_middleband'] = bollinger['mid'] - dataframe['bb_upperband'] = bollinger['upper'] - - # # EMA - Exponential Moving Average - # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) - - # # SMA - Simple Moving Average - # dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - - # SAR Parabol - dataframe['sar'] = ta.SAR(dataframe) - - # TEMA - Triple Exponential Moving Average - dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) - - # Cycle Indicator - # ------------------------------------ - # Hilbert Transform Indicator - SineWave - hilbert = ta.HT_SINE(dataframe) - dataframe['htsine'] = hilbert['sine'] - dataframe['htleadsine'] = hilbert['leadsine'] - - # Pattern Recognition - Bullish candlestick patterns - # ------------------------------------ - # # Hammer: values [0, 100] - # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - # # Inverted Hammer: values [0, 100] - # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - # # Dragonfly Doji: values [0, 100] - # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - # # Piercing Line: values [0, 100] - # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - # # Morningstar: values [0, 100] - # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - # # Three White Soldiers: values [0, 100] - # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - - # Pattern Recognition - Bearish candlestick patterns - # ------------------------------------ - # # Hanging Man: values [0, 100] - # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - # # Shooting Star: values [0, 100] - # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - # # Gravestone Doji: values [0, 100] - # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - # # Dark Cloud Cover: values [0, 100] - # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - # # Evening Doji Star: values [0, 100] - # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - # # Evening Star: values [0, 100] - # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - - # Pattern Recognition - Bullish/Bearish candlestick patterns - # ------------------------------------ - # # Three Line Strike: values [0, -100, 100] - # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - # # Spinning Top: values [0, -100, 100] - # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - # # Engulfing: values [0, -100, 100] - # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - # # Harami: values [0, -100, 100] - # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - # # Three Outside Up/Down: values [0, -100, 100] - # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - # # Three Inside Up/Down: values [0, -100, 100] - # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - - # # Chart type - # # ------------------------------------ - # # Heikinashi stategy - # heikinashi = qtpylib.heikinashi(dataframe) - # dataframe['ha_open'] = heikinashi['open'] - # dataframe['ha_close'] = heikinashi['close'] - # dataframe['ha_high'] = heikinashi['high'] - # dataframe['ha_low'] = heikinashi['low'] - - # Retrieve best bid and best ask from the orderbook - # ------------------------------------ - """ - # first check if dataprovider is available - if self.dp: - if self.dp.runmode in ('live', 'dry_run'): - ob = self.dp.orderbook(metadata['pair'], 1) - dataframe['best_bid'] = ob['bids'][0][0] - dataframe['best_ask'] = ob['asks'][0][0] - """ + {% filter indent(8) %}{% include 'subtemplates/indicators_' + subtemplates + '.j2' %}{% endfilter %} return dataframe @@ -275,9 +115,7 @@ class {{ strategy }}(IStrategy): """ dataframe.loc[ ( - (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 - (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle - (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising + {% filter indent(16) %}{% include 'subtemplates/buy_trend_' + subtemplates + '.j2' %}{% endfilter %} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), 'buy'] = 1 @@ -293,9 +131,7 @@ class {{ strategy }}(IStrategy): """ dataframe.loc[ ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 - (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle - (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling + {% filter indent(16) %}{% include 'subtemplates/sell_trend_' + subtemplates + '.j2' %}{% endfilter %} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), 'sell'] = 1 diff --git a/freqtrade/templates/subtemplates/buy_trend_full.j2 b/freqtrade/templates/subtemplates/buy_trend_full.j2 new file mode 100644 index 000000000..1a0d326b3 --- /dev/null +++ b/freqtrade/templates/subtemplates/buy_trend_full.j2 @@ -0,0 +1,3 @@ +(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 +(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle +(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 new file mode 100644 index 000000000..395808776 --- /dev/null +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -0,0 +1,161 @@ + +# Momentum Indicators +# ------------------------------------ + +# RSI +dataframe['rsi'] = ta.RSI(dataframe) + +# ADX +dataframe['adx'] = ta.ADX(dataframe) + +# # Aroon, Aroon Oscillator +# aroon = ta.AROON(dataframe) +# dataframe['aroonup'] = aroon['aroonup'] +# dataframe['aroondown'] = aroon['aroondown'] +# dataframe['aroonosc'] = ta.AROONOSC(dataframe) + +# # Awesome oscillator +# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + +# # Commodity Channel Index: values Oversold:<-100, Overbought:>100 +# dataframe['cci'] = ta.CCI(dataframe) + +# MACD +macd = ta.MACD(dataframe) +dataframe['macd'] = macd['macd'] +dataframe['macdsignal'] = macd['macdsignal'] +dataframe['macdhist'] = macd['macdhist'] + +# # MFI +# dataframe['mfi'] = ta.MFI(dataframe) + +# # Minus Directional Indicator / Movement +# dataframe['minus_dm'] = ta.MINUS_DM(dataframe) +# dataframe['minus_di'] = ta.MINUS_DI(dataframe) + +# # Plus Directional Indicator / Movement +# dataframe['plus_dm'] = ta.PLUS_DM(dataframe) +# dataframe['plus_di'] = ta.PLUS_DI(dataframe) +# dataframe['minus_di'] = ta.MINUS_DI(dataframe) + +# # ROC +# dataframe['roc'] = ta.ROC(dataframe) + +# # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) +# rsi = 0.1 * (dataframe['rsi'] - 50) +# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + +# # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) +# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + +# # Stoch +# stoch = ta.STOCH(dataframe) +# dataframe['slowd'] = stoch['slowd'] +# dataframe['slowk'] = stoch['slowk'] + +# Stoch fast +stoch_fast = ta.STOCHF(dataframe) +dataframe['fastd'] = stoch_fast['fastd'] +dataframe['fastk'] = stoch_fast['fastk'] + +# # Stoch RSI +# stoch_rsi = ta.STOCHRSI(dataframe) +# dataframe['fastd_rsi'] = stoch_rsi['fastd'] +# dataframe['fastk_rsi'] = stoch_rsi['fastk'] + +# Overlap Studies +# ------------------------------------ + +# Bollinger bands +bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) +dataframe['bb_lowerband'] = bollinger['lower'] +dataframe['bb_middleband'] = bollinger['mid'] +dataframe['bb_upperband'] = bollinger['upper'] + +# # EMA - Exponential Moving Average +# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) +# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) +# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) +# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) +# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + +# # SMA - Simple Moving Average +# dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + +# SAR Parabol +dataframe['sar'] = ta.SAR(dataframe) + +# TEMA - Triple Exponential Moving Average +dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + +# Cycle Indicator +# ------------------------------------ +# Hilbert Transform Indicator - SineWave +hilbert = ta.HT_SINE(dataframe) +dataframe['htsine'] = hilbert['sine'] +dataframe['htleadsine'] = hilbert['leadsine'] + +# Pattern Recognition - Bullish candlestick patterns +# ------------------------------------ +# # Hammer: values [0, 100] +# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) +# # Inverted Hammer: values [0, 100] +# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) +# # Dragonfly Doji: values [0, 100] +# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) +# # Piercing Line: values [0, 100] +# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] +# # Morningstar: values [0, 100] +# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] +# # Three White Soldiers: values [0, 100] +# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + +# Pattern Recognition - Bearish candlestick patterns +# ------------------------------------ +# # Hanging Man: values [0, 100] +# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) +# # Shooting Star: values [0, 100] +# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) +# # Gravestone Doji: values [0, 100] +# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) +# # Dark Cloud Cover: values [0, 100] +# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) +# # Evening Doji Star: values [0, 100] +# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) +# # Evening Star: values [0, 100] +# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + +# Pattern Recognition - Bullish/Bearish candlestick patterns +# ------------------------------------ +# # Three Line Strike: values [0, -100, 100] +# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) +# # Spinning Top: values [0, -100, 100] +# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] +# # Engulfing: values [0, -100, 100] +# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] +# # Harami: values [0, -100, 100] +# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] +# # Three Outside Up/Down: values [0, -100, 100] +# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] +# # Three Inside Up/Down: values [0, -100, 100] +# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + +# # Chart type +# # ------------------------------------ +# # Heikinashi stategy +# heikinashi = qtpylib.heikinashi(dataframe) +# dataframe['ha_open'] = heikinashi['open'] +# dataframe['ha_close'] = heikinashi['close'] +# dataframe['ha_high'] = heikinashi['high'] +# dataframe['ha_low'] = heikinashi['low'] + +# Retrieve best bid and best ask from the orderbook +# ------------------------------------ +""" +# first check if dataprovider is available +if self.dp: +if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] +""" diff --git a/freqtrade/templates/subtemplates/sell_trend_full.j2 b/freqtrade/templates/subtemplates/sell_trend_full.j2 new file mode 100644 index 000000000..36c08c947 --- /dev/null +++ b/freqtrade/templates/subtemplates/sell_trend_full.j2 @@ -0,0 +1,3 @@ +(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 +(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle +(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 3b37c6895..47bb5e3f6 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -106,7 +106,8 @@ def start_new_strategy(args: Dict[str, Any]) -> None: "Please choose another Strategy Name.") strategy_text = render_template(templatefile='base_strategy.py.j2', - arguments={"strategy": args["strategy"]}) + arguments={"strategy": args["strategy"], + "subtemplates": 'full'}) logger.info(f"Writing strategy to `{new_path}`.") new_path.write_text(strategy_text) From f26c40082dc806cd876debd9ccbfec0179f41ae2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Nov 2019 07:21:19 +0100 Subject: [PATCH 270/319] Allow selection of templates for strategy --- freqtrade/configuration/arguments.py | 4 ++-- freqtrade/configuration/cli_options.py | 8 ++++++++ .../templates/subtemplates/buy_trend_minimal.j2 | 1 + .../templates/subtemplates/indicators_full.j2 | 8 ++++---- .../subtemplates/indicators_minimal.j2 | 17 +++++++++++++++++ .../subtemplates/sell_trend_minimal.j2 | 1 + freqtrade/utils.py | 6 ++++-- 7 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 freqtrade/templates/subtemplates/buy_trend_minimal.j2 create mode 100644 freqtrade/templates/subtemplates/indicators_minimal.j2 create mode 100644 freqtrade/templates/subtemplates/sell_trend_minimal.j2 diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 5cc56a8bc..b23366d7a 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -39,9 +39,9 @@ ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] -ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy"] +ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] -ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt"] +ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index d7a496aa7..be9397975 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -339,6 +339,14 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), + # Templating options + "template": Arg( + '--template', + help='Use a template which is either `minimal` or ' + '`full` (containing multiple sample indicators).', + choices=['full', 'minimal'], + default='full', + ), # Plot dataframe "indicators1": Arg( '--indicators1', diff --git a/freqtrade/templates/subtemplates/buy_trend_minimal.j2 b/freqtrade/templates/subtemplates/buy_trend_minimal.j2 new file mode 100644 index 000000000..6a4079cf3 --- /dev/null +++ b/freqtrade/templates/subtemplates/buy_trend_minimal.j2 @@ -0,0 +1 @@ +(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 index 395808776..33dd85311 100644 --- a/freqtrade/templates/subtemplates/indicators_full.j2 +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -154,8 +154,8 @@ dataframe['htleadsine'] = hilbert['leadsine'] """ # first check if dataprovider is available if self.dp: -if self.dp.runmode in ('live', 'dry_run'): - ob = self.dp.orderbook(metadata['pair'], 1) - dataframe['best_bid'] = ob['bids'][0][0] - dataframe['best_ask'] = ob['asks'][0][0] + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] """ diff --git a/freqtrade/templates/subtemplates/indicators_minimal.j2 b/freqtrade/templates/subtemplates/indicators_minimal.j2 new file mode 100644 index 000000000..7d75b4610 --- /dev/null +++ b/freqtrade/templates/subtemplates/indicators_minimal.j2 @@ -0,0 +1,17 @@ + +# Momentum Indicators +# ------------------------------------ + +# RSI +dataframe['rsi'] = ta.RSI(dataframe) + +# Retrieve best bid and best ask from the orderbook +# ------------------------------------ +""" +# first check if dataprovider is available +if self.dp: + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] +""" diff --git a/freqtrade/templates/subtemplates/sell_trend_minimal.j2 b/freqtrade/templates/subtemplates/sell_trend_minimal.j2 new file mode 100644 index 000000000..42a7b81a2 --- /dev/null +++ b/freqtrade/templates/subtemplates/sell_trend_minimal.j2 @@ -0,0 +1 @@ +(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 47bb5e3f6..4657e58fc 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -107,7 +107,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None: strategy_text = render_template(templatefile='base_strategy.py.j2', arguments={"strategy": args["strategy"], - "subtemplates": 'full'}) + "subtemplates": args['template']}) logger.info(f"Writing strategy to `{new_path}`.") new_path.write_text(strategy_text) @@ -130,7 +130,9 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: "Please choose another Strategy Name.") strategy_text = render_template(templatefile='base_hyperopt.py.j2', - arguments={"hyperopt": args["hyperopt"]}) + arguments={"hyperopt": args["hyperopt"], + "subtemplates": args['template'] + }) logger.info(f"Writing hyperopt to `{new_path}`.") new_path.write_text(strategy_text) From f23f659ac590618ec8a4b232d49db079b61796d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Nov 2019 19:28:53 +0100 Subject: [PATCH 271/319] Use strings instead of subtemplates --- freqtrade/misc.py | 2 +- freqtrade/templates/base_strategy.py.j2 | 6 +++--- freqtrade/utils.py | 9 ++++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 6497a4727..bcba78cf0 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -129,7 +129,7 @@ def plural(num, singular: str, plural: str = None) -> str: return singular if (num == 1 or num == -1) else plural or singular + 's' -def render_template(templatefile: str, arguments: dict): +def render_template(templatefile: str, arguments: dict = {}): from jinja2 import Environment, PackageLoader, select_autoescape diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 4c5fe9a0b..73a4c7a5a 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -102,7 +102,7 @@ class {{ strategy }}(IStrategy): :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ - {% filter indent(8) %}{% include 'subtemplates/indicators_' + subtemplates + '.j2' %}{% endfilter %} + {{ indicators | indent(8) }} return dataframe @@ -115,7 +115,7 @@ class {{ strategy }}(IStrategy): """ dataframe.loc[ ( - {% filter indent(16) %}{% include 'subtemplates/buy_trend_' + subtemplates + '.j2' %}{% endfilter %} + {{ buy_trend | indent(16) }} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), 'buy'] = 1 @@ -131,7 +131,7 @@ class {{ strategy }}(IStrategy): """ dataframe.loc[ ( - {% filter indent(16) %}{% include 'subtemplates/sell_trend_' + subtemplates + '.j2' %}{% endfilter %} + {{ sell_trend | indent(16) }} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), 'sell'] = 1 diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 4657e58fc..e94de4f3e 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -105,9 +105,16 @@ def start_new_strategy(args: Dict[str, Any]) -> None: raise OperationalException(f"`{new_path}` already exists. " "Please choose another Strategy Name.") + indicators = render_template(templatefile=f"subtemplates/indicators_{args['template']}.j2",) + buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{args['template']}.j2",) + sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{args['template']}.j2",) + strategy_text = render_template(templatefile='base_strategy.py.j2', arguments={"strategy": args["strategy"], - "subtemplates": args['template']}) + "indicators": indicators, + "buy_trend": buy_trend, + "sell_trend": sell_trend, + }) logger.info(f"Writing strategy to `{new_path}`.") new_path.write_text(strategy_text) From 5f8fcebb8860831c4794e05318cecb36189cf5b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Nov 2019 19:41:57 +0100 Subject: [PATCH 272/319] Parametrize hyperopt file --- freqtrade/templates/base_hyperopt.py.j2 | 40 ++----------------- .../subtemplates/hyperopt_buy_guards_full.j2 | 8 ++++ .../hyperopt_buy_guards_minimal.j2 | 2 + .../subtemplates/hyperopt_buy_space_full.j2 | 9 +++++ .../hyperopt_buy_space_minimal.j2 | 3 ++ .../subtemplates/hyperopt_sell_guards_full.j2 | 8 ++++ .../hyperopt_sell_guards_minimal.j2 | 2 + .../subtemplates/hyperopt_sell_space_full.j2 | 11 +++++ .../hyperopt_sell_space_minimal.j2 | 5 +++ freqtrade/utils.py | 14 ++++++- 10 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 create mode 100644 freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 create mode 100644 freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 create mode 100644 freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 create mode 100644 freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 create mode 100644 freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 create mode 100644 freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 create mode 100644 freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 index c0f4e3292..05ba08b81 100644 --- a/freqtrade/templates/base_hyperopt.py.j2 +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -48,14 +48,7 @@ class {{ hyperopt }}(IHyperOpt): 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']) + {{ buy_guards | indent(12) }} # TRIGGERS if 'trigger' in params: @@ -85,15 +78,7 @@ class {{ hyperopt }}(IHyperOpt): Define your Hyperopt space for searching buy 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') + {{ buy_space | indent(12) }} ] @staticmethod @@ -108,14 +93,7 @@ class {{ hyperopt }}(IHyperOpt): 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']) + {{ sell_guards | indent(12) }} # TRIGGERS if 'sell-trigger' in params: @@ -145,15 +123,5 @@ class {{ hyperopt }}(IHyperOpt): 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') + {{ sell_space | indent(12) }} ] diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 new file mode 100644 index 000000000..5b967f4ed --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 @@ -0,0 +1,8 @@ +if params.get('mfi-enabled'): + conditions.append(dataframe['mfi'] < params['mfi-value']) +if params.get('fastd-enabled'): + conditions.append(dataframe['fastd'] < params['fastd-value']) +if params.get('adx-enabled'): + conditions.append(dataframe['adx'] > params['adx-value']) +if params.get('rsi-enabled'): + conditions.append(dataframe['rsi'] < params['rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 new file mode 100644 index 000000000..5e1022f59 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 @@ -0,0 +1,2 @@ +if params.get('rsi-enabled'): + conditions.append(dataframe['rsi'] < params['rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 new file mode 100644 index 000000000..29bafbd93 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 @@ -0,0 +1,9 @@ +Integer(10, 25, name='mfi-value'), +Integer(15, 45, name='fastd-value'), +Integer(20, 50, name='adx-value'), +Integer(20, 40, name='rsi-value'), +Categorical([True, False], name='mfi-enabled'), +Categorical([True, False], name='fastd-enabled'), +Categorical([True, False], name='adx-enabled'), +Categorical([True, False], name='rsi-enabled'), +Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 new file mode 100644 index 000000000..5ddf537fb --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 @@ -0,0 +1,3 @@ +Integer(20, 40, name='rsi-value'), +Categorical([True, False], name='rsi-enabled'), +Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 new file mode 100644 index 000000000..bd7b499f4 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 @@ -0,0 +1,8 @@ +if params.get('sell-mfi-enabled'): + conditions.append(dataframe['mfi'] > params['sell-mfi-value']) +if params.get('sell-fastd-enabled'): + conditions.append(dataframe['fastd'] > params['sell-fastd-value']) +if params.get('sell-adx-enabled'): + conditions.append(dataframe['adx'] < params['sell-adx-value']) +if params.get('sell-rsi-enabled'): + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 new file mode 100644 index 000000000..8b4adebf6 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 @@ -0,0 +1,2 @@ +if params.get('sell-rsi-enabled'): + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 new file mode 100644 index 000000000..46469d532 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 @@ -0,0 +1,11 @@ +Integer(75, 100, name='sell-mfi-value'), +Integer(50, 100, name='sell-fastd-value'), +Integer(50, 100, name='sell-adx-value'), +Integer(60, 100, name='sell-rsi-value'), +Categorical([True, False], name='sell-mfi-enabled'), +Categorical([True, False], name='sell-fastd-enabled'), +Categorical([True, False], name='sell-adx-enabled'), +Categorical([True, False], name='sell-rsi-enabled'), +Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 new file mode 100644 index 000000000..dfb110543 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 @@ -0,0 +1,5 @@ +Integer(60, 100, name='sell-rsi-value'), +Categorical([True, False], name='sell-rsi-enabled'), +Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') diff --git a/freqtrade/utils.py b/freqtrade/utils.py index e94de4f3e..e7b6eff4a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -136,9 +136,21 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: raise OperationalException(f"`{new_path}` already exists. " "Please choose another Strategy Name.") + buy_guards = render_template( + templatefile=f"subtemplates/hyperopt_buy_guards_{args['template']}.j2",) + sell_guards = render_template( + templatefile=f"subtemplates/hyperopt_sell_guards_{args['template']}.j2",) + buy_space = render_template( + templatefile=f"subtemplates/hyperopt_buy_space_{args['template']}.j2",) + sell_space = render_template( + templatefile=f"subtemplates/hyperopt_sell_space_{args['template']}.j2",) + strategy_text = render_template(templatefile='base_hyperopt.py.j2', arguments={"hyperopt": args["hyperopt"], - "subtemplates": args['template'] + "buy_guards": buy_guards, + "sell_guards": sell_guards, + "buy_space": buy_space, + "sell_space": sell_space, }) logger.info(f"Writing hyperopt to `{new_path}`.") From 210d468a9b1cff8c0d5373de185f60102d8e92e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Nov 2019 20:01:08 +0100 Subject: [PATCH 273/319] Reinstate mfi ... --- freqtrade/templates/sample_strategy.py | 4 +- .../templates/subtemplates/indicators_full.j2 | 4 +- freqtrade/utils.py | 78 +++++++++++-------- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 38a45c1f2..02bf24e7e 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -131,8 +131,8 @@ class SampleStrategy(IStrategy): dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdhist'] = macd['macdhist'] - # # MFI - # dataframe['mfi'] = ta.MFI(dataframe) + # MFI + dataframe['mfi'] = ta.MFI(dataframe) # # Minus Directional Indicator / Movement # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 index 33dd85311..879a2daa0 100644 --- a/freqtrade/templates/subtemplates/indicators_full.j2 +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -26,8 +26,8 @@ dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdhist'] = macd['macdhist'] -# # MFI -# dataframe['mfi'] = ta.MFI(dataframe) +# MFI +dataframe['mfi'] = ta.MFI(dataframe) # # Minus Directional Indicator / Movement # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index e7b6eff4a..c71080d5a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -91,6 +91,25 @@ def start_create_userdir(args: Dict[str, Any]) -> None: sys.exit(1) +def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str): + """ + Deploy new strategy from template to strategy_path + """ + indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",) + buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",) + sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",) + + strategy_text = render_template(templatefile='base_strategy.py.j2', + arguments={"strategy": strategy_name, + "indicators": indicators, + "buy_trend": buy_trend, + "sell_trend": sell_trend, + }) + + logger.info(f"Writing strategy to `{strategy_path}`.") + strategy_path.write_text(strategy_text) + + def start_new_strategy(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -105,23 +124,37 @@ def start_new_strategy(args: Dict[str, Any]) -> None: raise OperationalException(f"`{new_path}` already exists. " "Please choose another Strategy Name.") - indicators = render_template(templatefile=f"subtemplates/indicators_{args['template']}.j2",) - buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{args['template']}.j2",) - sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{args['template']}.j2",) + deploy_new_strategy(args['strategy'], new_path, args['template']) - strategy_text = render_template(templatefile='base_strategy.py.j2', - arguments={"strategy": args["strategy"], - "indicators": indicators, - "buy_trend": buy_trend, - "sell_trend": sell_trend, - }) - - logger.info(f"Writing strategy to `{new_path}`.") - new_path.write_text(strategy_text) else: raise OperationalException("`new-strategy` requires --strategy to be set.") +def deploy_new_hyperopt(hyperopt_name, hyperopt_path: Path, subtemplate: str): + """ + Deploys a new hyperopt template to hyperopt_path + """ + buy_guards = render_template( + templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",) + sell_guards = render_template( + templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",) + buy_space = render_template( + templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",) + sell_space = render_template( + templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",) + + strategy_text = render_template(templatefile='base_hyperopt.py.j2', + arguments={"hyperopt": hyperopt_name, + "buy_guards": buy_guards, + "sell_guards": sell_guards, + "buy_space": buy_space, + "sell_space": sell_space, + }) + + logger.info(f"Writing hyperopt to `{hyperopt_path}`.") + hyperopt_path.write_text(strategy_text) + + def start_new_hyperopt(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -135,26 +168,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: if new_path.exists(): raise OperationalException(f"`{new_path}` already exists. " "Please choose another Strategy Name.") - - buy_guards = render_template( - templatefile=f"subtemplates/hyperopt_buy_guards_{args['template']}.j2",) - sell_guards = render_template( - templatefile=f"subtemplates/hyperopt_sell_guards_{args['template']}.j2",) - buy_space = render_template( - templatefile=f"subtemplates/hyperopt_buy_space_{args['template']}.j2",) - sell_space = render_template( - templatefile=f"subtemplates/hyperopt_sell_space_{args['template']}.j2",) - - strategy_text = render_template(templatefile='base_hyperopt.py.j2', - arguments={"hyperopt": args["hyperopt"], - "buy_guards": buy_guards, - "sell_guards": sell_guards, - "buy_space": buy_space, - "sell_space": sell_space, - }) - - logger.info(f"Writing hyperopt to `{new_path}`.") - new_path.write_text(strategy_text) + deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) else: raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") From a6bb7595e850c1c1d0e4cb5bc8401ab250c63138 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Nov 2019 13:44:50 +0100 Subject: [PATCH 274/319] Update utils doc --- docs/utils.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/utils.md b/docs/utils.md index 26d354206..b07008b91 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -59,6 +59,7 @@ freqtrade new-strategy --userdir ~/.freqtrade/ --strategy AwesomeStrategy ``` output usage: freqtrade new-strategy [-h] [--userdir PATH] [-s NAME] + [--template {full,minimal}] optional arguments: -h, --help show this help message and exit @@ -67,6 +68,9 @@ optional arguments: -s NAME, --strategy NAME Specify strategy class name which will be used by the bot. + --template {full,minimal} + Use a template which is either `minimal` or `full` + (containing multiple sample indicators). ``` ## Create new hyperopt @@ -92,6 +96,7 @@ freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt ``` output usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME] + [--template {full,minimal}] optional arguments: -h, --help show this help message and exit @@ -99,6 +104,9 @@ optional arguments: Path to userdata directory. --hyperopt NAME Specify hyperopt class name which will be used by the bot. + --template {full,minimal} + Use a template which is either `minimal` or `full` + (containing multiple sample indicators). ``` ## List Exchanges From 097cdcb57ab358dad716b78cf02e481a48e5df35 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 23 Nov 2019 11:32:33 +0300 Subject: [PATCH 275/319] Save epochs at intermediate points --- freqtrade/optimize/hyperopt.py | 46 ++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6ea2f5133..ecfaba209 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -77,6 +77,8 @@ class Hyperopt: # Previous evaluations self.trials: List = [] + self.num_trials_saved = 0 + # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_indicators'): self.backtesting.strategy.advise_indicators = \ @@ -132,13 +134,17 @@ class Hyperopt: arg_dict = {dim.name: value for dim, value in zip(dimensions, params)} return arg_dict - def save_trials(self) -> None: + def save_trials(self, final: bool = False) -> None: """ Save hyperopt trials to file """ - if self.trials: - logger.info("Saving %d evaluations to '%s'", len(self.trials), self.trials_file) + num_trials = len(self.trials) + if num_trials > self.num_trials_saved: + logger.info(f"Saving {num_trials} epochs.") dump(self.trials, self.trials_file) + self.num_trials_saved = num_trials + if final: + logger.info(f"{num_trials} epochs were saved to '{self.trials_file}'.") def read_trials(self) -> List: """ @@ -153,6 +159,12 @@ class Hyperopt: """ Display Best hyperopt result """ + # This is printed when Ctrl+C is pressed quickly, before first epochs have + # a chance to be evaluated. + if not self.trials: + print("No epochs evaluated yet, no best result.") + return + results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] params = best_result['params'] @@ -197,12 +209,20 @@ class Hyperopt: # Also round to 5 digits after the decimal point print(f"Stoploss: {round(params.get('stoploss'), 5)}") + def is_best(self, results) -> bool: + return results['loss'] < self.current_best_loss + def log_results(self, results) -> None: """ Log results if it is better than any previous evaluation """ print_all = self.config.get('print_all', False) - is_best_loss = results['loss'] < self.current_best_loss + is_best_loss = self.is_best(results) + + if not print_all: + print('.', end='' if results['current_epoch'] % 100 != 0 else None) + sys.stdout.flush() + if print_all or is_best_loss: if is_best_loss: self.current_best_loss = results['loss'] @@ -217,13 +237,9 @@ class Hyperopt: print(log_str) else: print(f'\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 + current = results['current_epoch'] total = self.total_epochs res = results['results_explanation'] loss = results['loss'] @@ -422,15 +438,19 @@ class Hyperopt: self.opt.tell(asked, [v['loss'] for v in f_val]) self.fix_optimizer_models_list() for j in range(jobs): - current = i * jobs + j + # Use human-friendly index here (starting from 1) + current = i * jobs + j + 1 val = f_val[j] val['current_epoch'] = current - val['is_initial_point'] = current < INITIAL_POINTS + val['is_initial_point'] = current <= INITIAL_POINTS + logger.debug(f"Optimizer epoch evaluated: {val}") + is_best = self.is_best(val) self.log_results(val) self.trials.append(val) - logger.debug(f"Optimizer epoch evaluated: {val}") + if is_best or current % 100 == 0: + self.save_trials() except KeyboardInterrupt: print('User interrupted..') - self.save_trials() + self.save_trials(final=True) self.log_trials_result() From 737c07c5b68bb31a22aaa5cc3e3af0de72c45c04 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 23 Nov 2019 11:51:52 +0300 Subject: [PATCH 276/319] Make mypy happy --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ecfaba209..e536960c4 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -220,7 +220,7 @@ class Hyperopt: is_best_loss = self.is_best(results) if not print_all: - print('.', end='' if results['current_epoch'] % 100 != 0 else None) + print('.', end='' if results['current_epoch'] % 100 != 0 else None) # type: ignore sys.stdout.flush() if print_all or is_best_loss: From 99db53417ccadfc45409eef6bb8301d049ab1f74 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 23 Nov 2019 12:00:43 +0300 Subject: [PATCH 277/319] Tests adjusted --- tests/optimize/test_hyperopt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index e19835e58..00477f790 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -360,7 +360,7 @@ def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.log_results( { 'loss': 1, - 'current_epoch': 1, + 'current_epoch': 2, # This starts from 1 (in a human-friendly manner) 'results_explanation': 'foo.', 'is_initial_point': False } @@ -374,6 +374,7 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: hyperopt.log_results( { 'loss': 3, + 'current_epoch': 1, } ) assert caplog.record_tuples == [] @@ -386,7 +387,7 @@ def test_save_trials_saves_trials(mocker, hyperopt, testdatadir, caplog) -> None hyperopt.save_trials() trials_file = testdatadir / 'optimize' / 'ut_trials.pickle' - assert log_has(f"Saving 1 evaluations to '{trials_file}'", caplog) + assert log_has("Saving 1 epochs.", caplog) mock_dump.assert_called_once() From 067267f4cfc7a0442c4ae400c05be8a47d693333 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 23 Nov 2019 12:20:41 +0300 Subject: [PATCH 278/319] Log messages improved (plural/singular) --- freqtrade/optimize/hyperopt.py | 7 ++++--- tests/optimize/test_hyperopt.py | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e536960c4..836309a62 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from skopt import Optimizer from skopt.space import Dimension from freqtrade.data.history import get_timeframe, trim_dataframe -from freqtrade.misc import round_dict +from freqtrade.misc import plural, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4 @@ -140,11 +140,12 @@ class Hyperopt: """ num_trials = len(self.trials) if num_trials > self.num_trials_saved: - logger.info(f"Saving {num_trials} epochs.") + logger.info(f"Saving {num_trials} {plural(num_trials, 'epoch')}.") dump(self.trials, self.trials_file) self.num_trials_saved = num_trials if final: - logger.info(f"{num_trials} epochs were saved to '{self.trials_file}'.") + logger.info(f"{num_trials} {plural(num_trials, 'epoch')} " + f"saved to '{self.trials_file}'.") def read_trials(self) -> List: """ diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 00477f790..2ec9f5664 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -384,10 +384,11 @@ def test_save_trials_saves_trials(mocker, hyperopt, testdatadir, caplog) -> None trials = create_trials(mocker, hyperopt, testdatadir) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) hyperopt.trials = trials - hyperopt.save_trials() + hyperopt.save_trials(final=True) trials_file = testdatadir / 'optimize' / 'ut_trials.pickle' - assert log_has("Saving 1 epochs.", caplog) + assert log_has("Saving 1 epoch.", caplog) + assert log_has(f"1 epoch saved to '{trials_file}'.", caplog) mock_dump.assert_called_once() From 6cb48305343a478105ec7762ce3a5abb8b810a55 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 23 Nov 2019 12:30:49 +0300 Subject: [PATCH 279/319] Testcase added --- tests/optimize/test_hyperopt.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 2ec9f5664..d3d544502 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -383,14 +383,19 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: def test_save_trials_saves_trials(mocker, hyperopt, testdatadir, caplog) -> None: trials = create_trials(mocker, hyperopt, testdatadir) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) + trials_file = testdatadir / 'optimize' / 'ut_trials.pickle' + hyperopt.trials = trials hyperopt.save_trials(final=True) - - trials_file = testdatadir / 'optimize' / 'ut_trials.pickle' assert log_has("Saving 1 epoch.", caplog) assert log_has(f"1 epoch saved to '{trials_file}'.", caplog) mock_dump.assert_called_once() + hyperopt.trials = trials + trials + hyperopt.save_trials(final=True) + assert log_has("Saving 2 epochs.", caplog) + assert log_has(f"2 epochs saved to '{trials_file}'.", caplog) + def test_read_trials_returns_trials_file(mocker, hyperopt, testdatadir, caplog) -> None: trials = create_trials(mocker, hyperopt, testdatadir) From 5fb14e769b69e790a3a0f24820b24c690415c7bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2019 14:52:44 +0100 Subject: [PATCH 280/319] Adjust folder to match user_data folder - otherwise running tests creates this folder --- tests/test_plotting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index ec4df9125..31502cafc 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -212,9 +212,9 @@ def test_generate_plot_file(mocker, caplog): fig = generate_empty_figure() plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html", - directory=Path("user_data/plots")) + directory=Path("user_data/plot")) - expected_fn = str(Path("user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")) + expected_fn = str(Path("user_data/plot/freqtrade-plot-UNITTEST_BTC-5m.html")) assert plot_mock.call_count == 1 assert plot_mock.call_args[0][0] == fig assert (plot_mock.call_args_list[0][1]['filename'] From 63ad95a474b54eeab12b4f874c5dfbb5771657e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2019 20:13:20 +0100 Subject: [PATCH 281/319] reenable slack --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c74b1720e..99f3b3eec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,7 @@ jobs: # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" export CI_BRANCH=${GITHUB_REF#"ref/heads"} + export CI_BRANCH=${HEAD_REF} echo "${CI_BRANCH}" coveralls || true @@ -97,7 +98,7 @@ jobs: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 - if: always() && github.repository.fork == true + if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} job_name: '*Freqtrade CI ${{ matrix.os }}*' @@ -158,7 +159,7 @@ jobs: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 - if: always() && github.repository.fork == true + if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} job_name: '*Freqtrade CI windows*' @@ -178,7 +179,7 @@ jobs: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 - if: failure() && github.repository.fork == true + if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} job_name: '*Freqtrade Docs*' @@ -219,7 +220,7 @@ jobs: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 - if: always() && github.repository.fork == true + if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} job_name: '*Freqtrade CI Deploy*' From f05818a86e64f7194a24dc74451cb9b1641e3de6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2019 19:48:15 +0100 Subject: [PATCH 282/319] Allow transition from "no-config"-pairlist to pairlists --- freqtrade/configuration/deprecated_settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 8f3dbd675..b1e3535a3 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -58,6 +58,13 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', 'experimental', 'ignore_roi_if_buy_signal') + if not config.get('pairlists') and not config.get('pairlists'): + config['pairlists'] = [{'method': 'StaticPairList'}] + logger.warning( + "DEPRECATED: " + "Pairlists must be defined explicitly in the future." + "Defaulting to StaticPairList for now.") + if config.get('pairlist', {}).get("method") == 'VolumePairList': logger.warning( "DEPRECATED: " From cbf710a4f86f6d54ff56deb1f3f83fabbc592425 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2019 19:48:33 +0100 Subject: [PATCH 283/319] Fix coveralls (?) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99f3b3eec..ea5f9c395 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,9 +73,9 @@ jobs: # Allow failure for coveralls # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" - export CI_BRANCH=${GITHUB_REF#"ref/heads"} - export CI_BRANCH=${HEAD_REF} - echo "${CI_BRANCH}" + export TRAVIS_BRANCH=${GITHUB_REF#"ref/heads"} + export TRAVIS_BRANCH=${HEAD_REF} + echo "${TRAVIS_BRANCH}" coveralls || true - name: Backtesting From a374df76225cdb7f4a3bc93462336708ad650e8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Nov 2019 09:55:34 +0100 Subject: [PATCH 284/319] some minor fixes from feedback --- docs/strategy-customization.md | 2 +- docs/utils.md | 7 +++++-- freqtrade/configuration/cli_options.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 352389d5e..c43d8e3f6 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -22,7 +22,7 @@ The bot includes a default strategy file. Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). You will however most likely have your own idea for a strategy. -This Document intends to help you develop one for yourself. +This document intends to help you develop one for yourself. To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`. This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`. diff --git a/docs/utils.md b/docs/utils.md index b07008b91..ca4b645a5 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -70,7 +70,9 @@ optional arguments: bot. --template {full,minimal} Use a template which is either `minimal` or `full` - (containing multiple sample indicators). + (containing multiple sample indicators). Default: + `full`. + ``` ## Create new hyperopt @@ -106,7 +108,8 @@ optional arguments: bot. --template {full,minimal} Use a template which is either `minimal` or `full` - (containing multiple sample indicators). + (containing multiple sample indicators). Default: + `full`. ``` ## List Exchanges diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index be9397975..2061534e7 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -343,7 +343,7 @@ AVAILABLE_CLI_OPTIONS = { "template": Arg( '--template', help='Use a template which is either `minimal` or ' - '`full` (containing multiple sample indicators).', + '`full` (containing multiple sample indicators). Default: `%(default)s`.', choices=['full', 'minimal'], default='full', ), From 8c64be3cfd618ba3e69dc30c1cb4096d4a56b693 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Nov 2019 07:01:23 +0100 Subject: [PATCH 285/319] get tickers only once to show balance --- freqtrade/rpc/rpc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8898f3068..f2851e0e1 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -301,6 +301,7 @@ class RPC: """ Returns current account balance per crypto """ output = [] total = 0.0 + tickers = self._freqtrade.exchange.get_tickers() for coin, balance in self._freqtrade.exchange.get_balances().items(): if not balance['total']: continue @@ -310,10 +311,11 @@ class RPC: else: try: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC") + if pair.startswith("BTC"): - rate = 1.0 / self._freqtrade.get_sell_rate(pair, False) + rate = 1.0 / tickers.get(pair, {}).get('bid', 1) else: - rate = self._freqtrade.get_sell_rate(pair, False) + rate = tickers.get(pair, {}).get('bid', 1) except (TemporaryError, DependencyException): logger.warning(f" Could not get rate for pair {coin}.") continue From 62d50f512d82c9988897425b5013f718ae039f34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Nov 2019 20:12:41 +0100 Subject: [PATCH 286/319] add tests for balance from get-tickers --- freqtrade/rpc/rpc.py | 6 +++- tests/conftest.py | 50 +++++++++++++++++++++++++++++++-- tests/pairlist/test_pairlist.py | 16 +++++------ tests/rpc/test_rpc.py | 38 +++++++++---------------- tests/rpc/test_rpc_telegram.py | 23 ++------------- 5 files changed, 75 insertions(+), 58 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f2851e0e1..e338cd6dd 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -301,7 +301,11 @@ class RPC: """ Returns current account balance per crypto """ output = [] total = 0.0 - tickers = self._freqtrade.exchange.get_tickers() + try: + tickers = self._freqtrade.exchange.get_tickers() + except (TemporaryError, DependencyException): + raise RPCException('Error getting current tickers.') + for coin, balance in self._freqtrade.exchange.get_balances().items(): if not balance['total']: continue diff --git a/tests/conftest.py b/tests/conftest.py index fbd23a0dc..bc6599e4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -980,6 +980,28 @@ def tickers(): 'quoteVolume': 62.68220262, 'info': {} }, + 'BTC/USDT': { + 'symbol': 'BTC/USDT', + 'timestamp': 1573758371399, + 'datetime': '2019-11-14T19:06:11.399Z', + 'high': 8800.0, + 'low': 8582.6, + 'bid': 8648.16, + 'bidVolume': 0.238771, + 'ask': 8648.72, + 'askVolume': 0.016253, + 'vwap': 8683.13647806, + 'open': 8759.7, + 'close': 8648.72, + 'last': 8648.72, + 'previousClose': 8759.67, + 'change': -110.98, + 'percentage': -1.267, + 'average': None, + 'baseVolume': 35025.943355, + 'quoteVolume': 304135046.4242901, + 'info': {} + }, 'ETH/USDT': { 'symbol': 'ETH/USDT', 'timestamp': 1522014804118, @@ -1067,7 +1089,29 @@ def tickers(): 'baseVolume': 59698.79897, 'quoteVolume': 29132399.743954, 'info': {} - } + }, + 'XRP/BTC': { + 'symbol': 'XRP/BTC', + 'timestamp': 1573758257534, + 'datetime': '2019-11-14T19:04:17.534Z', + 'high': 3.126e-05, + 'low': 3.061e-05, + 'bid': 3.093e-05, + 'bidVolume': 27901.0, + 'ask': 3.095e-05, + 'askVolume': 10551.0, + 'vwap': 3.091e-05, + 'open': 3.119e-05, + 'close': 3.094e-05, + 'last': 3.094e-05, + 'previousClose': 3.117e-05, + 'change': -2.5e-07, + 'percentage': -0.802, + 'average': None, + 'baseVolume': 37334921.0, + 'quoteVolume': 1154.19266394, + 'info': {} + }, }) @@ -1317,8 +1361,8 @@ def rpc_balance(): 'used': 0.0 }, 'XRP': { - 'total': 1.0, - 'free': 1.0, + 'total': 0.1, + 'free': 0.01, 'used': 0.0 }, 'EUR': { diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 76537880c..460f2ddcf 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -100,7 +100,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co markets=PropertyMock(return_value=shitcoinmarkets), ) # argument: use the whitelist dynamically by exchange-volume - whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC'] + whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC'] bot.pairlists.refresh_pairlist() assert whitelist == bot.pairlists.whitelist @@ -135,10 +135,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): @pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [ ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), # Different sorting depending on quote or bid volume ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], - "BTC", ['HOT/BTC', 'FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']), + "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "USDT", ['ETH/USDT']), # No pair for ETH ... @@ -146,19 +146,19 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "ETH", []), # Precisionfilter and quote volume ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']), + {"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), # Precisionfilter bid ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, - {"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']), + {"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), # PriceFilter and VolumePairList ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.03}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']), + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), # Hot is removed by precision_filter, Fuel by low_price_filter. - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.02} - ], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + ], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), # StaticPairlist Only ([{"method": "StaticPairList"}, ], "BTC", ['ETH/BTC', 'TKN/BTC']), diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index fb7a5276a..d745212ac 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -355,29 +355,18 @@ def test_rpc_balance_handle_error(default_conf, mocker): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=mock_balance), - get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx')) + get_tickers=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx')) ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() - - result = rpc._rpc_balance(default_conf['fiat_display_currency']) - assert prec_satoshi(result['total'], 12) - assert prec_satoshi(result['value'], 180000) - assert 'USD' == result['symbol'] - assert result['currencies'] == [{ - 'currency': 'BTC', - 'free': 10.0, - 'balance': 12.0, - 'used': 2.0, - 'est_btc': 12.0, - }] - assert result['total'] == 12.0 + with pytest.raises(RPCException, match="Error getting current tickers."): + rpc._rpc_balance(default_conf['fiat_display_currency']) -def test_rpc_balance_handle(default_conf, mocker): +def test_rpc_balance_handle(default_conf, mocker, tickers): mock_balance = { 'BTC': { 'free': 10.0, @@ -389,7 +378,7 @@ def test_rpc_balance_handle(default_conf, mocker): 'total': 5.0, 'used': 4.0, }, - 'PAX': { + 'USDT': { 'free': 5.0, 'total': 10.0, 'used': 5.0, @@ -405,10 +394,9 @@ def test_rpc_balance_handle(default_conf, mocker): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=mock_balance), - get_ticker=MagicMock( - side_effect=lambda p, r: {'bid': 100} if p == "BTC/PAX" else {'bid': 0.01}), + get_tickers=tickers, get_valid_pair_combination=MagicMock( - side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}") + side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -417,8 +405,8 @@ def test_rpc_balance_handle(default_conf, mocker): rpc._fiat_converter = CryptoToFiatConverter() result = rpc._rpc_balance(default_conf['fiat_display_currency']) - assert prec_satoshi(result['total'], 12.15) - assert prec_satoshi(result['value'], 182250) + assert prec_satoshi(result['total'], 12.309096315) + assert prec_satoshi(result['value'], 184636.44472997) assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', @@ -430,16 +418,16 @@ def test_rpc_balance_handle(default_conf, mocker): {'free': 1.0, 'balance': 5.0, 'currency': 'ETH', - 'est_btc': 0.05, + 'est_btc': 0.30794, 'used': 4.0 }, {'free': 5.0, 'balance': 10.0, - 'currency': 'PAX', - 'est_btc': 0.1, + 'currency': 'USDT', + 'est_btc': 0.0011563153318162476, 'used': 5.0} ] - assert result['total'] == 12.15 + assert result['total'] == 12.309096315331816 def test_rpc_start(mocker, default_conf) -> None: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a33ab8675..89fd90b0b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -461,29 +461,10 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] -def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> None: - - def mock_ticker(symbol, refresh): - if symbol == 'BTC/USDT': - return { - 'bid': 10000.00, - 'ask': 10000.00, - 'last': 10000.00, - } - elif symbol == 'XRP/BTC': - return { - 'bid': 0.00001, - 'ask': 0.00001, - 'last': 0.00001, - } - return { - 'bid': 0.1, - 'ask': 0.1, - 'last': 0.1, - } +def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") From 1bf8d8cff320da2b8e05fd64102faaf33e786e2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Nov 2019 06:33:07 +0100 Subject: [PATCH 287/319] show /balance in stake currency --- freqtrade/rpc/api_server.py | 3 ++- freqtrade/rpc/rpc.py | 24 +++++++++++++----------- freqtrade/rpc/telegram.py | 7 ++++--- tests/rpc/test_rpc.py | 25 +++++++++++++++---------- tests/rpc/test_rpc_apiserver.py | 3 ++- tests/rpc/test_rpc_telegram.py | 3 ++- 6 files changed, 38 insertions(+), 27 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index f87165253..4baca7f22 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -354,7 +354,8 @@ class ApiServer(RPC): Returns the current status of the trades in json format """ - results = self._rpc_balance(self._config.get('fiat_display_currency', '')) + results = self._rpc_balance(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) return self.rest_dump(results) @require_login diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e338cd6dd..137e72ea6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -297,7 +297,7 @@ class RPC: 'best_rate': round(bp_rate * 100, 2), } - def _rpc_balance(self, fiat_display_currency: str) -> Dict: + def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ output = [] total = 0.0 @@ -310,27 +310,29 @@ class RPC: if not balance['total']: continue - if coin == 'BTC': + est_stake: float = 0 + if coin == stake_currency: rate = 1.0 + est_stake = balance['total'] else: try: - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC") - - if pair.startswith("BTC"): - rate = 1.0 / tickers.get(pair, {}).get('bid', 1) - else: - rate = tickers.get(pair, {}).get('bid', 1) + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) + rate = tickers.get(pair, {}).get('bid', None) + if rate: + if pair.startswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance['total'] except (TemporaryError, DependencyException): logger.warning(f" Could not get rate for pair {coin}.") continue - est_btc: float = rate * balance['total'] - total = total + est_btc + total = total + (est_stake or 0) output.append({ 'currency': coin, 'free': balance['free'] if balance['free'] is not None else 0, 'balance': balance['total'] if balance['total'] is not None else 0, 'used': balance['used'] if balance['used'] is not None else 0, - 'est_btc': est_btc, + 'est_stake': est_stake or 0, + 'stake': stake_currency, }) if total == 0.0: if self._freqtrade.config.get('dry_run', False): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0547af7b0..2ae22f472 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -325,15 +325,16 @@ class Telegram(RPC): def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ try: - result = self._rpc_balance(self._config.get('fiat_display_currency', '')) + result = self._rpc_balance(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) output = '' for currency in result['currencies']: - if currency['est_btc'] > 0.0001: + if currency['est_stake'] > 0.0001: curr_output = "*{currency}:*\n" \ "\t`Available: {free: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ "\t`Pending: {used: .8f}`\n" \ - "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) + "\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency) else: curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d745212ac..2c7228274 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -363,7 +363,7 @@ def test_rpc_balance_handle_error(default_conf, mocker): rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() with pytest.raises(RPCException, match="Error getting current tickers."): - rpc._rpc_balance(default_conf['fiat_display_currency']) + rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) def test_rpc_balance_handle(default_conf, mocker, tickers): @@ -404,28 +404,33 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() - result = rpc._rpc_balance(default_conf['fiat_display_currency']) + result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', - 'free': 10.0, - 'balance': 12.0, - 'used': 2.0, - 'est_btc': 12.0, + 'free': 10.0, + 'balance': 12.0, + 'used': 2.0, + 'est_stake': 12.0, + 'stake': 'BTC', }, {'free': 1.0, 'balance': 5.0, 'currency': 'ETH', - 'est_btc': 0.30794, - 'used': 4.0 + 'est_stake': 0.30794, + 'used': 4.0, + 'stake': 'BTC', + }, {'free': 5.0, 'balance': 10.0, 'currency': 'USDT', - 'est_btc': 0.0011563153318162476, - 'used': 5.0} + 'est_stake': 0.0011563153318162476, + 'used': 5.0, + 'stake': 'BTC', + } ] assert result['total'] == 12.309096315331816 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8eff37023..4dc3fd265 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -256,7 +256,8 @@ def test_api_balance(botclient, mocker, rpc_balance): 'free': 12.0, 'balance': 12.0, 'used': 0.0, - 'est_btc': 12.0, + 'est_stake': 12.0, + 'stake': 'BTC', } diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 89fd90b0b..c848a3efd 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -545,7 +545,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'free': 1.0, 'used': 0.5, 'balance': i, - 'est_btc': 1 + 'est_stake': 1, + 'stake': 'BTC', }) mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ 'currencies': balances, From 50350a09cd494c802a1586cfaec9bf78ef876c4f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Nov 2019 19:41:51 +0100 Subject: [PATCH 288/319] use wallets instead of doing a direct call to /balance --- freqtrade/rpc/rpc.py | 14 +++++++------- freqtrade/wallets.py | 5 ++++- tests/rpc/test_rpc_apiserver.py | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 137e72ea6..4cebe646e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -306,14 +306,14 @@ class RPC: except (TemporaryError, DependencyException): raise RPCException('Error getting current tickers.') - for coin, balance in self._freqtrade.exchange.get_balances().items(): - if not balance['total']: + for coin, balance in self._freqtrade.wallets.get_all_balances().items(): + if not balance.total: continue est_stake: float = 0 if coin == stake_currency: rate = 1.0 - est_stake = balance['total'] + est_stake = balance.total else: try: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) @@ -321,16 +321,16 @@ class RPC: if rate: if pair.startswith(stake_currency): rate = 1.0 / rate - est_stake = rate * balance['total'] + est_stake = rate * balance.total except (TemporaryError, DependencyException): logger.warning(f" Could not get rate for pair {coin}.") continue total = total + (est_stake or 0) output.append({ 'currency': coin, - 'free': balance['free'] if balance['free'] is not None else 0, - 'balance': balance['total'] if balance['total'] is not None else 0, - 'used': balance['used'] if balance['used'] is not None else 0, + 'free': balance.free if balance.free is not None else 0, + 'balance': balance.total if balance.total is not None else 0, + 'used': balance.used if balance.used is not None else 0, 'est_stake': est_stake or 0, 'stake': stake_currency, }) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 90b68c49d..c674b5286 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -2,7 +2,7 @@ """ Wallet """ import logging -from typing import Dict, NamedTuple +from typing import Dict, NamedTuple, Any from freqtrade.exchange import Exchange from freqtrade import constants @@ -72,3 +72,6 @@ class Wallets: ) logger.info('Wallets synced.') + + def get_all_balances(self) -> Dict[str, Any]: + return self._wallets diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4dc3fd265..7b3e787f4 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -243,9 +243,9 @@ def test_api_balance(botclient, mocker, rpc_balance): 'last': 0.1, } mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") + ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) From 1b337fe5e1c1cc67752aa8af6b0bf7f612038d96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Nov 2019 19:47:20 +0100 Subject: [PATCH 289/319] Remove unnecessary code piece --- tests/rpc/test_rpc_apiserver.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7b3e787f4..8d5b4a6b8 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -224,24 +224,6 @@ def test_api_stopbuy(botclient): def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient - def mock_ticker(symbol, refresh): - if symbol == 'BTC/USDT': - return { - 'bid': 10000.00, - 'ask': 10000.00, - 'last': 10000.00, - } - elif symbol == 'XRP/BTC': - return { - 'bid': 0.00001, - 'ask': 0.00001, - 'last': 0.00001, - } - return { - 'bid': 0.1, - 'ask': 0.1, - 'last': 0.1, - } mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") From a3415e52c0311e901be1267925ac26f23601ad0c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2019 15:20:53 +0100 Subject: [PATCH 290/319] Fix some test-types --- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 18 ++++++++++++------ tests/rpc/test_rpc_manager.py | 5 ++++- tests/rpc/test_rpc_telegram.py | 4 ++-- tests/rpc/test_rpc_webhook.py | 2 +- tests/test_freqtradebot.py | 11 ++++++----- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index fb7a5276a..0e8588aea 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -697,8 +697,8 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None pair = 'XRP/BTC' # Test not buying - default_conf['stake_amount'] = 0.0000001 freqtradebot = get_patched_freqtradebot(mocker, default_conf) + freqtradebot.config['stake_amount'] = 0.0000001 patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'TKN/BTC' diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8eff37023..a59fd1942 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -23,7 +23,7 @@ _TEST_PASS = "SuperSecurePassword1!" def botclient(default_conf, mocker): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080", + "listen_port": 8080, "username": _TEST_USER, "password": _TEST_PASS, }}) @@ -133,7 +133,10 @@ def test_api__init__(default_conf, mocker): def test_api_run(default_conf, mocker, caplog): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080"}}) + "listen_port": 8080, + "username": "TestUser", + "password": "testPass", + }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) @@ -146,7 +149,7 @@ def test_api_run(default_conf, mocker, caplog): apiserver.run() assert server_mock.call_count == 1 assert server_mock.call_args_list[0][0][0] == "127.0.0.1" - assert server_mock.call_args_list[0][0][1] == "8080" + assert server_mock.call_args_list[0][0][1] == 8080 assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert hasattr(apiserver, "srv") @@ -158,14 +161,14 @@ def test_api_run(default_conf, mocker, caplog): server_mock.reset_mock() apiserver._config.update({"api_server": {"enabled": True, "listen_ip_address": "0.0.0.0", - "listen_port": "8089", + "listen_port": 8089, "password": "", }}) apiserver.run() assert server_mock.call_count == 1 assert server_mock.call_args_list[0][0][0] == "0.0.0.0" - assert server_mock.call_args_list[0][0][1] == "8089" + 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) assert log_has("Starting Local Rest Server.", caplog) @@ -186,7 +189,10 @@ def test_api_run(default_conf, mocker, caplog): def test_api_cleanup(default_conf, mocker, caplog): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080"}}) + "listen_port": 8080, + "username": "TestUser", + "password": "testPass", + }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index c9fbf8c3b..edf6bae4d 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -173,7 +173,10 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: default_conf["telegram"]["enabled"] = False default_conf["api_server"] = {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080"} + "listen_port": 8080, + "username": "TestUser", + "password": "TestPass", + } rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) # Sleep to allow the thread to start diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a33ab8675..f69c2ac38 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -144,9 +144,9 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: def test_status(default_conf, update, mocker, fee, ticker,) -> None: - update.message.chat.id = 123 + update.message.chat.id = "123" default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = 123 + default_conf['telegram']['chat_id'] = "123" mocker.patch.multiple( 'freqtrade.exchange.Exchange', diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index dbbc4cefb..c066aa8e7 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -113,7 +113,7 @@ def test_send_msg(default_conf, mocker): def test_exception_send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() - default_conf["webhook"]["webhookbuy"] = None + del default_conf["webhook"]["webhookbuy"] webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b01c8e247..76a50f0f4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -299,7 +299,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, limit_buy_order, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - default_conf['stake_amount'] = 0.0000098751 + default_conf['stake_amount'] = 0.0098751 default_conf['max_open_trades'] = 2 mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -313,7 +313,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, trade = Trade.query.first() assert trade is not None - assert trade.stake_amount == 0.0000098751 + assert trade.stake_amount == 0.0098751 assert trade.is_open assert trade.open_date is not None @@ -321,11 +321,11 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, trade = Trade.query.order_by(Trade.id.desc()).first() assert trade is not None - assert trade.stake_amount == 0.0000098751 + assert trade.stake_amount == 0.0098751 assert trade.is_open assert trade.open_date is not None - assert Trade.total_open_trades_stakes() == 1.97502e-05 + assert Trade.total_open_trades_stakes() == 1.97502e-02 def test_get_min_pair_stake_amount(mocker, default_conf) -> None: @@ -522,8 +522,9 @@ def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_or get_fee=fee, ) - default_conf['stake_amount'] = 0.000000005 freqtrade = FreqtradeBot(default_conf) + freqtrade.config['stake_amount'] = 0.000000005 + patch_get_signal(freqtrade) assert not freqtrade.create_trades() From 4dc0631a4b8aa8e65ca7d85690f3e5b6dd560211 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2019 15:41:09 +0100 Subject: [PATCH 291/319] Lower minimum tradeable value --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index bf5d822c6..c16850c38 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -71,7 +71,7 @@ CONF_SCHEMA = { 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_amount': { "type": ["number", "string"], - "minimum": 0.0005, + "minimum": 0.0001, "pattern": UNLIMITED_STAKE_AMOUNT }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, From af3eea38055acdcf533f9810d282205bc315399e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2019 15:49:46 +0100 Subject: [PATCH 292/319] Move config json validation to after strategy loading Otherwise attributes are mandatory in configuration while they could be set in the strategy --- freqtrade/configuration/config_validation.py | 5 +++++ freqtrade/configuration/configuration.py | 8 -------- freqtrade/optimize/backtesting.py | 5 ++++- freqtrade/optimize/edge_cli.py | 5 ++++- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 8a7641a08..bfba59385 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -61,6 +61,11 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: :param conf: Config in JSON format :return: Returns None if everything is ok, otherwise throw an OperationalException """ + + # validate configuration before returning + logger.info('Validating configuration ...') + validate_config_schema(conf) + # validating trailing stoploss _validate_trailing_stoploss(conf) _validate_edge(conf) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 93eee3912..277bf8da9 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -9,8 +9,6 @@ from typing import Any, Callable, Dict, List, Optional from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.config_validation import (validate_config_consistency, - validate_config_schema) from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.directory_operations import (create_datadir, create_userdata_dir) @@ -84,10 +82,6 @@ class Configuration: if 'pairlists' not in config: config['pairlists'] = [] - # validate configuration before returning - logger.info('Validating configuration ...') - validate_config_schema(config) - return config def load_config(self) -> Dict[str, Any]: @@ -118,8 +112,6 @@ class Configuration: process_temporary_deprecated_settings(config) - validate_config_consistency(config) - return config def _process_logging_options(self, config: Dict[str, Any]) -> None: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2c2d116a4..d9fb1f2d1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,8 @@ from pandas import DataFrame from tabulate import tabulate from freqtrade import OperationalException -from freqtrade.configuration import TimeRange, remove_credentials +from freqtrade.configuration import (TimeRange, remove_credentials, + validate_config_consistency) from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -75,10 +76,12 @@ class Backtesting: stratconf = deepcopy(self.config) stratconf['strategy'] = strat self.strategylist.append(StrategyResolver(stratconf).strategy) + validate_config_consistency(stratconf) else: # No strategy list specified, only one strategy self.strategylist.append(StrategyResolver(self.config).strategy) + validate_config_consistency(self.config) if "ticker_interval" not in self.config: raise OperationalException("Ticker-interval needs to be set in either configuration " diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 5a4543884..a667ebb92 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -9,7 +9,8 @@ from typing import Any, Dict from tabulate import tabulate from freqtrade import constants -from freqtrade.configuration import TimeRange, remove_credentials +from freqtrade.configuration import (TimeRange, remove_credentials, + validate_config_consistency) from freqtrade.edge import Edge from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver @@ -35,6 +36,8 @@ class EdgeCli: self.exchange = Exchange(self.config) self.strategy = StrategyResolver(self.config).strategy + validate_config_consistency(self.config) + self.edge = Edge(config, self.exchange, self.strategy) # Set refresh_pairs to false for edge-cli (it must be true for edge) self.edge._refresh_pairs = False From 8d002a8f28cadf73df6f496bf3b2f0e29cfa3870 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2019 15:50:23 +0100 Subject: [PATCH 293/319] Fix some more tests --- tests/pairlist/test_pairlist.py | 5 ----- tests/test_configuration.py | 4 ---- 2 files changed, 9 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 76537880c..32d66d3e8 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -285,12 +285,7 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): - del whitelist_conf['pairlists'][0]['method'] mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - with pytest.raises(OperationalException, - match=r"No Pairlist defined!"): - get_patched_freqtradebot(mocker, whitelist_conf) - assert log_has_re("No method in .*", caplog) whitelist_conf['pairlists'] = [] diff --git a/tests/test_configuration.py b/tests/test_configuration.py index e971d15ab..e50ba99ee 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -100,7 +100,6 @@ 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) def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: @@ -132,7 +131,6 @@ 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) def test_from_config(default_conf, mocker, caplog) -> None: @@ -159,7 +157,6 @@ def test_from_config(default_conf, mocker, caplog) -> None: 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) assert isinstance(validated_conf['user_data_dir'], Path) @@ -191,7 +188,6 @@ 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) assert "runmode" in validated_conf assert validated_conf['runmode'] == RunMode.DRY_RUN From e7be742c58c9d4e1e3b15e7e4dc8f6979b96836a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Nov 2019 07:05:18 +0100 Subject: [PATCH 294/319] Run validation after custom validations --- freqtrade/configuration/config_validation.py | 8 ++++---- freqtrade/constants.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index bfba59385..4bfd24677 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -62,15 +62,15 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: :return: Returns None if everything is ok, otherwise throw an OperationalException """ - # validate configuration before returning - logger.info('Validating configuration ...') - validate_config_schema(conf) - # validating trailing stoploss _validate_trailing_stoploss(conf) _validate_edge(conf) _validate_whitelist(conf) + # validate configuration before returning + logger.info('Validating configuration ...') + validate_config_schema(conf) + def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c16850c38..22dcc9755 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -66,7 +66,7 @@ MINIMAL_CONFIG = { CONF_SCHEMA = { 'type': 'object', 'properties': { - 'max_open_trades': {'type': 'integer', 'minimum': -1}, + 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, 'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_amount': { From 646a9d12b20604a5536800af2a34ed667147f56f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Nov 2019 07:06:55 +0100 Subject: [PATCH 295/319] Align quoting of json schema --- freqtrade/constants.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 22dcc9755..58eb304a9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -70,9 +70,9 @@ CONF_SCHEMA = { 'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_amount': { - "type": ["number", "string"], - "minimum": 0.0001, - "pattern": UNLIMITED_STAKE_AMOUNT + 'type': ['number', 'string'], + 'minimum': 0.0001, + 'pattern': UNLIMITED_STAKE_AMOUNT }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, @@ -197,8 +197,8 @@ CONF_SCHEMA = { 'listen_ip_address': {'format': 'ipv4'}, 'listen_port': { 'type': 'integer', - "minimum": 1024, - "maximum": 65535 + 'minimum': 1024, + 'maximum': 65535 }, 'username': {'type': 'string'}, 'password': {'type': 'string'}, @@ -253,19 +253,19 @@ CONF_SCHEMA = { 'edge': { 'type': 'object', 'properties': { - "enabled": {'type': 'boolean'}, - "process_throttle_secs": {'type': 'integer', 'minimum': 600}, - "calculate_since_number_of_days": {'type': 'integer'}, - "allowed_risk": {'type': 'number'}, - "capital_available_percentage": {'type': 'number'}, - "stoploss_range_min": {'type': 'number'}, - "stoploss_range_max": {'type': 'number'}, - "stoploss_range_step": {'type': 'number'}, - "minimum_winrate": {'type': 'number'}, - "minimum_expectancy": {'type': 'number'}, - "min_trade_number": {'type': 'number'}, - "max_trade_duration_minute": {'type': 'integer'}, - "remove_pumps": {'type': 'boolean'} + 'enabled': {'type': 'boolean'}, + 'process_throttle_secs': {'type': 'integer', 'minimum': 600}, + 'calculate_since_number_of_days': {'type': 'integer'}, + 'allowed_risk': {'type': 'number'}, + 'capital_available_percentage': {'type': 'number'}, + 'stoploss_range_min': {'type': 'number'}, + 'stoploss_range_max': {'type': 'number'}, + 'stoploss_range_step': {'type': 'number'}, + 'minimum_winrate': {'type': 'number'}, + 'minimum_expectancy': {'type': 'number'}, + 'min_trade_number': {'type': 'number'}, + 'max_trade_duration_minute': {'type': 'integer'}, + 'remove_pumps': {'type': 'boolean'} }, 'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage'] } From 0775ac081a05a32446bcab3d30420b2874da1b63 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Nov 2019 07:12:30 +0100 Subject: [PATCH 296/319] Cleanup constants and required --- freqtrade/constants.py | 2 +- freqtrade/rpc/api_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 58eb304a9..b485ba0d8 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -6,7 +6,6 @@ bot constants DEFAULT_CONFIG = 'config.json' DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec -DEFAULT_TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' @@ -280,5 +279,6 @@ CONF_SCHEMA = { 'dry_run', 'bid_strategy', 'unfilledtimeout', + 'stoploss', ] } diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index f87165253..1ec8cc305 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -312,7 +312,7 @@ class ApiServer(RPC): logger.info("LocalRPC - Profit Command Called") stats = self._rpc_trade_statistics(self._config['stake_currency'], - self._config['fiat_display_currency'] + self._config.get('fiat_display_currency') ) return self.rest_dump(stats) From d1511a108577629b8df45e36d185ffabeb5e29b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Nov 2019 07:12:39 +0100 Subject: [PATCH 297/319] Update some config documentation --- docs/configuration.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f913d7dbb..bf7c07268 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,12 +40,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Command | Default | Description | |----------|---------|-------------| -| `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) -| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. -| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. +| `max_open_trades` | | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) +| `stake_currency` | | **Required.** Crypto-currency used for trading. +| `stake_amount` | | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. | `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. -| `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). -| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below. +| `ticker_interval` | 5m | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). +| `fiat_display_currency` | | Fiat currency used to show your profits. More information below. | `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode. | `dry_run_wallet` | 999.9 | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. | `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). @@ -94,8 +94,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. | `initial_state` | running | Defines the initial application state. More information below. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. -| `strategy` | None | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`. -| `strategy_path` | null | Adds an additional strategy lookup path (must be a directory). +| `strategy` | | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`. +| `strategy_path` | | Adds an additional strategy lookup path (must be a directory). | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.heartbeat_interval` | 60 | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages. | `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. From 6ab7f93ce766ac708ea8b29919633dc5a156631d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2019 07:20:40 +0000 Subject: [PATCH 298/319] Bump scipy from 1.3.2 to 1.3.3 Bumps [scipy](https://github.com/scipy/scipy) from 1.3.2 to 1.3.3. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.3.2...v1.3.3) Signed-off-by: dependabot-preview[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index ff8de9cb2..96a22b42e 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.3.2 +scipy==1.3.3 scikit-learn==0.21.3 scikit-optimize==0.5.2 filelock==3.0.12 From 28f73ecb3d02802f586cc27f15cbaed30769c886 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2019 07:21:16 +0000 Subject: [PATCH 299/319] Bump ccxt from 1.19.54 to 1.19.86 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.19.54 to 1.19.86. - [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.19.54...1.19.86) 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 2c176e9c3..ec6224c50 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.19.54 +ccxt==1.19.86 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 From 0a7a1290e3ba062deeacba3205141fc763a04cf1 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2019 07:21:32 +0000 Subject: [PATCH 300/319] Bump pytest-mock from 1.11.2 to 1.12.1 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 1.11.2 to 1.12.1. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v1.11.2...v1.12.1) 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 297b95623..34b52c963 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.740 pytest==5.2.4 pytest-asyncio==0.10.0 pytest-cov==2.8.1 -pytest-mock==1.11.2 +pytest-mock==1.12.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From 03f02294d1b351b415431cac82deed633b0cda60 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2019 07:21:52 +0000 Subject: [PATCH 301/319] Bump pytest from 5.2.4 to 5.3.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.2.4 to 5.3.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.2.4...5.3.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 297b95623..2dc8c88d9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.1.0 mypy==0.740 -pytest==5.2.4 +pytest==5.3.0 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==1.11.2 From 418ca0030521d5d1d827c12c348820aefd545e83 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2019 07:22:09 +0000 Subject: [PATCH 302/319] Bump jsonschema from 3.1.1 to 3.2.0 Bumps [jsonschema](https://github.com/Julian/jsonschema) from 3.1.1 to 3.2.0. - [Release notes](https://github.com/Julian/jsonschema/releases) - [Changelog](https://github.com/Julian/jsonschema/blob/master/CHANGELOG.rst) - [Commits](https://github.com/Julian/jsonschema/compare/v3.1.1...v3.2.0) 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 2c176e9c3..8f6c7e3d6 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -8,7 +8,7 @@ cachetools==3.1.1 requests==2.22.0 urllib3==1.25.7 wrapt==1.11.2 -jsonschema==3.1.1 +jsonschema==3.2.0 TA-Lib==0.4.17 tabulate==0.8.6 coinmarketcap==5.0.3 From e7c17df844d08656de128d0baf2e83276f5a71e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Nov 2019 12:56:05 +0100 Subject: [PATCH 303/319] validate defaults in documentation --- docs/configuration.md | 58 +++++++++++++++++++++--------------------- freqtrade/constants.py | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index bf7c07268..68ecd4629 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,67 +40,67 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Command | Default | Description | |----------|---------|-------------| -| `max_open_trades` | | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) +| `max_open_trades` | | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) | `stake_currency` | | **Required.** Crypto-currency used for trading. | `stake_amount` | | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. | `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. -| `ticker_interval` | 5m | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). +| `ticker_interval` | | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `fiat_display_currency` | | Fiat currency used to show your profits. More information below. | `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode. -| `dry_run_wallet` | 999.9 | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. +| `dry_run_wallet` | | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. | `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). -| `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). -| `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `minimal_roi` | | **Required.** Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). +| `stoploss` | | **Required.** Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_stop` | false | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_stop_positive` | 0 | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_stop_positive_offset` | 0 | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_only_offset_is_reached` | false | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `unfilledtimeout.buy` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. -| `unfilledtimeout.sell` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. -| `bid_strategy.ask_last_balance` | 0.0 | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). +| `unfilledtimeout.buy` | | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. +| `unfilledtimeout.sell` | | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. +| `bid_strategy.ask_last_balance` | | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). | `bid_strategy.use_order_book` | false | Allows buying of pair using the rates in Order Book Bids. -| `bid_strategy.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. +| `bid_strategy.order_book_top` | 1 | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. | `bid_strategy. check_depth_of_market.enabled` | false | Does not buy if the % difference of buy orders and sell orders is met in Order Book. | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. | `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks. -| `ask_strategy.order_book_min` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. -| `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. +| `ask_strategy.order_book_min` | 1 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. +| `ask_strategy.order_book_max` | 1 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `ask_strategy.use_sell_signal` | true | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `ask_strategy.sell_profit_only` | false | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `ask_strategy.ignore_roi_if_buy_signal` | false | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). -| `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). -| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). -| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). +| `order_types` | | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). +| `order_time_in_force` | | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). +| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. -| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.password` | '' | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.*** +| `exchange.key` | | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** +| `exchange.secret` | | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** +| `exchange.password` | | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.*** | `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). | `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). -| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) -| `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) +| `exchange.ccxt_config` | | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) +| `exchange.ccxt_async_config` | | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. -| `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation. +| `edge.*` | | Please refer to [edge configuration document](edge.md) for detailed explanation. | `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. | `pairlists` | StaticPairList | Define one or more pairlists to be used. [More information below](#dynamic-pairlists). -| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. -| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** -| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** +| `telegram.enabled` | | Enable the usage of Telegram. +| `telegram.token` | | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** +| `telegram.chat_id` | | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** | `webhook.enabled` | false | Enable usage of Webhook notifications -| `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. -| `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhooksell` | false | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhookstatus` | false | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `webhook.url` | | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. +| `webhook.webhookbuy` | | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `webhook.webhooksell` | | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `webhook.webhookstatus` | | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. | `initial_state` | running | Defines the initial application state. More information below. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. | `strategy` | | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`. | `strategy_path` | | Adds an additional strategy lookup path (must be a directory). -| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. +| `internals.process_throttle_secs` | 5 | Set the process throttle. Value in second. | `internals.heartbeat_interval` | 60 | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages. | `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 | Directory containing user data. Defaults to `./user_data/`. +| `user_data_dir` | | Directory containing user data. Defaults to `./user_data/`. ### Parameters in the strategy diff --git a/freqtrade/constants.py b/freqtrade/constants.py index b485ba0d8..f6e08bc36 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -93,8 +93,8 @@ CONF_SCHEMA = { 'unfilledtimeout': { 'type': 'object', 'properties': { - 'buy': {'type': 'number', 'minimum': 3}, - 'sell': {'type': 'number', 'minimum': 10} + 'buy': {'type': 'number', 'minimum': 1}, + 'sell': {'type': 'number', 'minimum': 1} } }, 'bid_strategy': { From 37f698d9c1269d5b817a2a52b01d25b4f32450e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Nov 2019 14:20:41 +0100 Subject: [PATCH 304/319] move default values to Description field --- docs/configuration.md | 72 +++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 68ecd4629..5241eaab9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -43,62 +43,62 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `max_open_trades` | | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) | `stake_currency` | | **Required.** Crypto-currency used for trading. | `stake_amount` | | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. -| `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. +| `amount_reserve_percent` | | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. *Defaults to `0.05` (5%).* | `ticker_interval` | | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `fiat_display_currency` | | Fiat currency used to show your profits. More information below. -| `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode. +| `dry_run` | | **Required.** Define if the bot must be in Dry-run or production mode. *Defaults to `true`.* | `dry_run_wallet` | | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. -| `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). +| `process_only_new_candles` | | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. *Defaults to `false`.* [Strategy Override](#parameters-in-the-strategy). | `minimal_roi` | | **Required.** Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). | `stoploss` | | **Required.** Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop` | false | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive` | 0 | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive_offset` | 0 | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_only_offset_is_reached` | false | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop` | | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop_positive` | | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop_positive_offset` | | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. *Defaults to `0.0` (no offset).* More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_only_offset_is_reached` | | Only apply trailing stoploss when the offset is reached. *Defaults to `false`.* [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `unfilledtimeout.buy` | | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. | `unfilledtimeout.sell` | | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. | `bid_strategy.ask_last_balance` | | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). -| `bid_strategy.use_order_book` | false | Allows buying of pair using the rates in Order Book Bids. -| `bid_strategy.order_book_top` | 1 | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. -| `bid_strategy. check_depth_of_market.enabled` | false | Does not buy if the % difference of buy orders and sell orders is met in Order Book. -| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. -| `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks. -| `ask_strategy.order_book_min` | 1 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. -| `ask_strategy.order_book_max` | 1 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. -| `ask_strategy.use_sell_signal` | true | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). -| `ask_strategy.sell_profit_only` | false | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). -| `ask_strategy.ignore_roi_if_buy_signal` | false | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). +| `bid_strategy.use_order_book` | | Enable buying using the rates in Order Book Bids. +| `bid_strategy.order_book_top` | | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.* +| `bid_strategy. check_depth_of_market.enabled` | | Does not buy if the difference of buy orders and sell orders is met in Order Book. *Defaults to `false`.* +| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.* +| `ask_strategy.use_order_book` | | Enable selling of open trades using Order Book Asks. +| `ask_strategy.order_book_min` | | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. *Defaults to `1`.* +| `ask_strategy.order_book_max` | | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. *Defaults to `1`.* +| `ask_strategy.use_sell_signal` | | Use sell signals produced by the strategy in addition to the `minimal_roi`. *Defaults to `true`.* [Strategy Override](#parameters-in-the-strategy). +| `ask_strategy.sell_profit_only` | | Wait until the bot makes a positive profit before taking a sell decision. *Defaults to `false`.* [Strategy Override](#parameters-in-the-strategy). +| `ask_strategy.ignore_roi_if_buy_signal` | | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. *Defaults to `false`.* [Strategy Override](#parameters-in-the-strategy). | `order_types` | | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). | `order_time_in_force` | | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). | `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). -| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. -| `exchange.key` | | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.secret` | | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.password` | | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). -| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). +| `exchange.sandbox` | | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. +| `exchange.key` | | API key to use for the exchange. Only required when you are in production mode. **Keep it in secrete, do not disclose publicly.** +| `exchange.secret` | | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secrete, do not disclose publicly.** +| `exchange.password` | | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secrete, do not disclose publicly.** +| `exchange.pair_whitelist` | | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). +| `exchange.pair_blacklist` | | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). | `exchange.ccxt_config` | | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) -| `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. +| `exchange.markets_refresh_interval` | | The interval in minutes in which markets are reloaded. *Defaults to `60` minutes.* | `edge.*` | | Please refer to [edge configuration document](edge.md) for detailed explanation. -| `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. -| `pairlists` | StaticPairList | Define one or more pairlists to be used. [More information below](#dynamic-pairlists). +| `experimental.block_bad_exchanges` | | Block exchanges known to not work with freqtrade. *Defaults to `true`.* Leave on default unless you want to test if that exchange works now. +| `pairlists` | | Define one or more pairlists to be used. *Defaults to `StaticPairList`.* [More information below](#dynamic-pairlists). | `telegram.enabled` | | Enable the usage of Telegram. -| `telegram.token` | | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** -| `telegram.chat_id` | | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** -| `webhook.enabled` | false | Enable usage of Webhook notifications +| `telegram.token` | | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secrete, do not disclose publicly.** +| `telegram.chat_id` | | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secrete, do not disclose publicly.** +| `webhook.enabled` | | Enable usage of Webhook notifications | `webhook.url` | | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. | `webhook.webhookbuy` | | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhooksell` | | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `webhook.webhooksell` | | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhookstatus` | | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. -| `initial_state` | running | Defines the initial application state. More information below. -| `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. +| `db_url` | | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`, and to `sqlite:///tradesv3.sqlite` for production instances. +| `initial_state` | | Defines the initial application state. More information below. *Defaults to `stopped`.* +| `forcebuy_enable` | | Enables the RPC Commands to force a buy. More information below. | `strategy` | | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`. | `strategy_path` | | Adds an additional strategy lookup path (must be a directory). -| `internals.process_throttle_secs` | 5 | Set the process throttle. Value in second. -| `internals.heartbeat_interval` | 60 | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages. -| `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. +| `internals.process_throttle_secs` | | Set the process throttle. Value in second. *Defaults to `5` seconds.* +| `internals.heartbeat_interval` | | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages. *Defaults to `60` seconds.* +| `internals.sd_notify` | | 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` | | Directory containing user data. Defaults to `./user_data/`. From 12b9257c6d321e3ef34a3a32acd27b1f7cafb8d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Nov 2019 14:25:02 +0100 Subject: [PATCH 305/319] new-lines before defaults in documentation --- docs/configuration.md | 126 +++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5241eaab9..a29621585 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -38,69 +38,69 @@ The prevelance for all Options is as follows: Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways. -| Command | Default | Description | -|----------|---------|-------------| -| `max_open_trades` | | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) -| `stake_currency` | | **Required.** Crypto-currency used for trading. -| `stake_amount` | | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. -| `amount_reserve_percent` | | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. *Defaults to `0.05` (5%).* -| `ticker_interval` | | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). -| `fiat_display_currency` | | Fiat currency used to show your profits. More information below. -| `dry_run` | | **Required.** Define if the bot must be in Dry-run or production mode. *Defaults to `true`.* -| `dry_run_wallet` | | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. -| `process_only_new_candles` | | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. *Defaults to `false`.* [Strategy Override](#parameters-in-the-strategy). -| `minimal_roi` | | **Required.** Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). -| `stoploss` | | **Required.** Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop` | | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive` | | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive_offset` | | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. *Defaults to `0.0` (no offset).* More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_only_offset_is_reached` | | Only apply trailing stoploss when the offset is reached. *Defaults to `false`.* [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `unfilledtimeout.buy` | | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. -| `unfilledtimeout.sell` | | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. -| `bid_strategy.ask_last_balance` | | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). -| `bid_strategy.use_order_book` | | Enable buying using the rates in Order Book Bids. -| `bid_strategy.order_book_top` | | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.* -| `bid_strategy. check_depth_of_market.enabled` | | Does not buy if the difference of buy orders and sell orders is met in Order Book. *Defaults to `false`.* -| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.* -| `ask_strategy.use_order_book` | | Enable selling of open trades using Order Book Asks. -| `ask_strategy.order_book_min` | | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. *Defaults to `1`.* -| `ask_strategy.order_book_max` | | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. *Defaults to `1`.* -| `ask_strategy.use_sell_signal` | | Use sell signals produced by the strategy in addition to the `minimal_roi`. *Defaults to `true`.* [Strategy Override](#parameters-in-the-strategy). -| `ask_strategy.sell_profit_only` | | Wait until the bot makes a positive profit before taking a sell decision. *Defaults to `false`.* [Strategy Override](#parameters-in-the-strategy). -| `ask_strategy.ignore_roi_if_buy_signal` | | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. *Defaults to `false`.* [Strategy Override](#parameters-in-the-strategy). -| `order_types` | | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). -| `order_time_in_force` | | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). -| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). -| `exchange.sandbox` | | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. -| `exchange.key` | | API key to use for the exchange. Only required when you are in production mode. **Keep it in secrete, do not disclose publicly.** -| `exchange.secret` | | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secrete, do not disclose publicly.** -| `exchange.password` | | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secrete, do not disclose publicly.** -| `exchange.pair_whitelist` | | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). -| `exchange.pair_blacklist` | | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). -| `exchange.ccxt_config` | | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) -| `exchange.ccxt_async_config` | | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) -| `exchange.markets_refresh_interval` | | The interval in minutes in which markets are reloaded. *Defaults to `60` minutes.* -| `edge.*` | | Please refer to [edge configuration document](edge.md) for detailed explanation. -| `experimental.block_bad_exchanges` | | Block exchanges known to not work with freqtrade. *Defaults to `true`.* Leave on default unless you want to test if that exchange works now. -| `pairlists` | | Define one or more pairlists to be used. *Defaults to `StaticPairList`.* [More information below](#dynamic-pairlists). -| `telegram.enabled` | | Enable the usage of Telegram. -| `telegram.token` | | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secrete, do not disclose publicly.** -| `telegram.chat_id` | | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secrete, do not disclose publicly.** -| `webhook.enabled` | | Enable usage of Webhook notifications -| `webhook.url` | | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. -| `webhook.webhookbuy` | | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhooksell` | | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhookstatus` | | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `db_url` | | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`, and to `sqlite:///tradesv3.sqlite` for production instances. -| `initial_state` | | Defines the initial application state. More information below. *Defaults to `stopped`.* -| `forcebuy_enable` | | Enables the RPC Commands to force a buy. More information below. -| `strategy` | | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`. -| `strategy_path` | | Adds an additional strategy lookup path (must be a directory). -| `internals.process_throttle_secs` | | Set the process throttle. Value in second. *Defaults to `5` seconds.* -| `internals.heartbeat_interval` | | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages. *Defaults to `60` seconds.* -| `internals.sd_notify` | | 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` | | Directory containing user data. Defaults to `./user_data/`. +| Command | Description | +|----------|-------------| +| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) +| `stake_currency` | **Required.** Crypto-currency used for trading. +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. +| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).* +| `ticker_interval` | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). +| `fiat_display_currency` | Fiat currency used to show your profits. More information below. +| `dry_run` | **Required.** Define if the bot must be in Dry-run or production mode.
    *Defaults to `true`.* +| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. +| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle.
    *Defaults to `false`.* [Strategy Override](#parameters-in-the-strategy). +| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). +| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop` | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop_positive` | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive.
    *Defaults to `0.0` (no offset).* More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* +| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. +| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. +| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). +| `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids. +| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.* +| `bid_strategy. check_depth_of_market.enabled` | Does not buy if the difference of buy orders and sell orders is met in Order Book.
    *Defaults to `false`.* +| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.* +| `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks. +| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
    *Defaults to `1`.* +| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
    *Defaults to `1`.* +| `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `true`.* +| `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* +| `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* +| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). +| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). +| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). +| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. +| `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secrete, do not disclose publicly.** +| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secrete, do not disclose publicly.** +| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secrete, do not disclose publicly.** +| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). +| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). +| `exchange.ccxt_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) +| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) +| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
    *Defaults to `60` minutes.* +| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. +| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
    *Defaults to `true`.* +| `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
    *Defaults to `StaticPairList`.* +| `telegram.enabled` | Enable the usage of Telegram. +| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secrete, do not disclose publicly.** +| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secrete, do not disclose publicly.** +| `webhook.enabled` | Enable usage of Webhook notifications +| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. +| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`, and to `sqlite:///tradesv3.sqlite` for production instances. +| `initial_state` | Defines the initial application state. More information below.
    *Defaults to `stopped`.* +| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. +| `strategy` | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`. +| `strategy_path` | Adds an additional strategy lookup path (must be a directory). +| `internals.process_throttle_secs` | Set the process throttle. Value in second.
    *Defaults to `5` seconds.* +| `internals.heartbeat_interval` | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages.
    *Defaults to `60` seconds.* +| `internals.sd_notify` | 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` | Directory containing user data.
    *Defaults to `./user_data/`*. ### Parameters in the strategy From 9e7d367b5c30989729848b37b0f5ba8f5eb45208 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Nov 2019 15:43:09 +0100 Subject: [PATCH 306/319] Realign strategy_override paramters --- docs/configuration.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a29621585..296c19d36 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -41,19 +41,19 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Command | Description | |----------|-------------| | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) -| `stake_currency` | **Required.** Crypto-currency used for trading. -| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. +| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. [Strategy Override](#parameters-in-the-strategy). | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).* | `ticker_interval` | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `fiat_display_currency` | Fiat currency used to show your profits. More information below. | `dry_run` | **Required.** Define if the bot must be in Dry-run or production mode.
    *Defaults to `true`.* | `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. -| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle.
    *Defaults to `false`.* [Strategy Override](#parameters-in-the-strategy). +| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_stop` | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_stop_positive` | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive.
    *Defaults to `0.0` (no offset).* More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `0.0` (no offset).* | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. @@ -107,15 +107,18 @@ Mandatory parameters are marked as **Required**, which means that they are requi The following parameters can be set in either configuration file or strategy. Values set in the configuration file always overwrite values set in the strategy. -* `ticker_interval` * `minimal_roi` +* `ticker_interval` * `stoploss` * `trailing_stop` * `trailing_stop_positive` * `trailing_stop_positive_offset` +* `trailing_only_offset_is_reached` * `process_only_new_candles` * `order_types` * `order_time_in_force` +* `stake_currency` +* `stake_amount` * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) From 8204107315fdda493c254b592c86adb448c2b157 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Nov 2019 11:57:02 +0300 Subject: [PATCH 307/319] Add test for get_min_pair_stake_amount() with real data --- tests/test_freqtradebot.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b01c8e247..937723073 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -334,6 +334,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.stoploss = -0.05 markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + # no pair found mocker.patch( 'freqtrade.exchange.Exchange.markets', @@ -440,6 +441,25 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: assert result == min(8, 2 * 2) / 0.9 +def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.stoploss = -0.05 + markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + + # Real Binance data + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 0.0001}, + 'amount': {'min': 0.001} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 0.020405) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) + def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From 17269c88bef96618cf91bbe6ceabbefa3ae37c39 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Nov 2019 11:57:58 +0300 Subject: [PATCH 308/319] Fix _get_min_pair_stake_amount() --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 358c63f90..b5d157635 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -266,7 +266,7 @@ class FreqtradeBot: amount_reserve_percent += self.strategy.stoploss # it should not be more than 50% amount_reserve_percent = max(amount_reserve_percent, 0.5) - return min(min_stake_amounts) / amount_reserve_percent + return max(min_stake_amounts) / amount_reserve_percent def create_trades(self) -> bool: """ From 0ac592ad40a473a8a7adfc4ab7319d14c2a00632 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Nov 2019 12:00:20 +0300 Subject: [PATCH 309/319] Fix markets in conftest --- tests/conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fbd23a0dc..bf245a840 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -325,7 +325,7 @@ def get_markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -351,7 +351,7 @@ def get_markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -376,7 +376,7 @@ def get_markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -401,7 +401,7 @@ def get_markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -426,7 +426,7 @@ def get_markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -451,7 +451,7 @@ def get_markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -479,7 +479,7 @@ def get_markets(): 'max': None }, 'cost': { - 'min': 0.001, + 'min': 0.0001, 'max': None } }, From 8e1e20bf0d536e8b4b46c8ea26757a180419e080 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Nov 2019 12:07:43 +0300 Subject: [PATCH 310/319] Fix some tests --- tests/test_freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 937723073..746a05cc6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -299,7 +299,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, limit_buy_order, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - default_conf['stake_amount'] = 0.0000098751 + default_conf['stake_amount'] = 0.00098751 default_conf['max_open_trades'] = 2 mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -313,7 +313,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, trade = Trade.query.first() assert trade is not None - assert trade.stake_amount == 0.0000098751 + assert trade.stake_amount == 0.00098751 assert trade.is_open assert trade.open_date is not None @@ -321,11 +321,11 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, trade = Trade.query.order_by(Trade.id.desc()).first() assert trade is not None - assert trade.stake_amount == 0.0000098751 + assert trade.stake_amount == 0.00098751 assert trade.is_open assert trade.open_date is not None - assert Trade.total_open_trades_stakes() == 1.97502e-05 + assert Trade.total_open_trades_stakes() == 1.97502e-03 def test_get_min_pair_stake_amount(mocker, default_conf) -> None: From 066f32406058b22b0d3c227745cd205ec6ab3418 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Nov 2019 12:28:04 +0300 Subject: [PATCH 311/319] Make flake happy --- tests/test_freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 746a05cc6..31e9f8750 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -460,6 +460,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 0.020405) assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) + def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From 585b8332ad61098fe95bce3e674c559363377dc3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2019 11:48:01 +0100 Subject: [PATCH 312/319] Improve tests and unify required attribute --- freqtrade/constants.py | 4 +--- tests/test_configuration.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f6e08bc36..0d52bf405 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -269,10 +269,8 @@ CONF_SCHEMA = { 'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage'] } }, - 'anyOf': [ - {'required': ['exchange']} - ], 'required': [ + 'exchange', 'max_open_trades', 'stake_currency', 'stake_amount', diff --git a/tests/test_configuration.py b/tests/test_configuration.py index e50ba99ee..60bd6d7df 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -40,10 +40,16 @@ def test_load_config_invalid_pair(default_conf) -> None: def test_load_config_missing_attributes(default_conf) -> None: - default_conf.pop('exchange') + conf = deepcopy(default_conf) + conf.pop('exchange') with pytest.raises(ValidationError, match=r".*'exchange' is a required property.*"): - validate_config_schema(default_conf) + validate_config_schema(conf) + + conf = deepcopy(default_conf) + conf.pop('stake_currency') + with pytest.raises(ValidationError, match=r".*'stake_currency' is a required property.*"): + validate_config_schema(conf) def test_load_config_incorrect_stake_amount(default_conf) -> None: From cceb00c4065c1a66ff7995f7a59304365e75b046 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Nov 2019 12:12:41 +0100 Subject: [PATCH 313/319] Try coveralls --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 047a34dd6..f6a111944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" export TRAVIS_BRANCH=${GITHUB_REF#"ref/heads"} - export TRAVIS_BRANCH=${HEAD_REF} + export CI_BRANCH=${GITHUB_REF#"ref/heads"} echo "${TRAVIS_BRANCH}" coveralls || true From f2cd4fdafe989bd6d8713d19126d1dfdf8a646ba Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 27 Nov 2019 05:12:54 +0300 Subject: [PATCH 314/319] Fix the rest of tests --- tests/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 31e9f8750..841bf8a6a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -426,7 +426,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == min(2, 2 * 2) / 0.9 + assert result == max(2, 2 * 2) / 0.9 # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -438,7 +438,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == min(8, 2 * 2) / 0.9 + assert result == max(8, 2 * 2) / 0.9 def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: From a373e48939f74c73091d7d010988861c64cf2ee4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 27 Nov 2019 14:53:01 +0300 Subject: [PATCH 315/319] Comment added --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b5d157635..ec341ff0a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -266,6 +266,10 @@ class FreqtradeBot: amount_reserve_percent += self.strategy.stoploss # it should not be more than 50% amount_reserve_percent = max(amount_reserve_percent, 0.5) + + # The value returned should satisfy both limits: for amount (base currency) and + # for cost (quote, stake currency), so max() is used here. + # See also #2575 at github. return max(min_stake_amounts) / amount_reserve_percent def create_trades(self) -> bool: From f0e6a9e0e3973452e18c562cd2188e202210e2ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Nov 2019 14:18:40 +0100 Subject: [PATCH 316/319] Address feedback --- docs/bot-usage.md | 6 +++--- docs/configuration.md | 40 ++++++++++++++++++++-------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4665878d4..25818aea6 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -89,9 +89,9 @@ The bot allows you to use multiple configuration files by specifying multiple defined in the latter configuration files override parameters with the same name 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 example, you can make a separate configuration file with your key and secret for the Exchange you use for trading, specify default configuration file with -empty key and secrete values while running in the Dry Mode (which does not actually +empty key and secret values while running in the Dry Mode (which does not actually require them): ```bash @@ -104,7 +104,7 @@ and specify both configuration files when running in the normal Live Trade Mode: freqtrade trade -c ./config.json -c path/to/secrets/keys.config.json ``` -This could help you hide your private Exchange key and Exchange secrete on you local machine +This could help you hide your private Exchange key and Exchange secret on you local machine by setting appropriate file permissions for the file which contains actual secrets and, additionally, prevent unintended disclosure of sensitive private data when you publish examples of your configuration in the project issues or in the Internet. diff --git a/docs/configuration.md b/docs/configuration.md index 296c19d36..76bfe8339 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -43,16 +43,16 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. [Strategy Override](#parameters-in-the-strategy). -| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).* -| `ticker_interval` | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). -| `fiat_display_currency` | Fiat currency used to show your profits. More information below. -| `dry_run` | **Required.** Define if the bot must be in Dry-run or production mode.
    *Defaults to `true`.* +| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).* +| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). +| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). +| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
    *Defaults to `true`.* | `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* -| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). -| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop` | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive` | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). +| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). +| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `0.0` (no offset).* | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. @@ -60,7 +60,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). | `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids. | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.* -| `bid_strategy. check_depth_of_market.enabled` | Does not buy if the difference of buy orders and sell orders is met in Order Book.
    *Defaults to `false`.* +| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book.
    *Defaults to `false`.* | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.* | `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks. | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
    *Defaults to `1`.* @@ -72,9 +72,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. -| `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secrete, do not disclose publicly.** -| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secrete, do not disclose publicly.** -| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secrete, do not disclose publicly.** +| `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.** +| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.** +| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secret, do not disclose publicly.** | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). | `exchange.ccxt_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) @@ -84,22 +84,22 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
    *Defaults to `true`.* | `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
    *Defaults to `StaticPairList`.* | `telegram.enabled` | Enable the usage of Telegram. -| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secrete, do not disclose publicly.** -| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secrete, do not disclose publicly.** +| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.** +| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.** | `webhook.enabled` | Enable usage of Webhook notifications | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. | `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`, and to `sqlite:///tradesv3.sqlite` for production instances. +| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. | `initial_state` | Defines the initial application state. More information below.
    *Defaults to `stopped`.* -| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. -| `strategy` | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`. +| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. +| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. | `strategy_path` | Adds an additional strategy lookup path (must be a directory). | `internals.process_throttle_secs` | Set the process throttle. Value in second.
    *Defaults to `5` seconds.* -| `internals.heartbeat_interval` | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages.
    *Defaults to `60` seconds.* +| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages.
    *Defaults to `60` seconds.* | `internals.sd_notify` | 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. +| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. | `user_data_dir` | Directory containing user data.
    *Defaults to `./user_data/`*. ### Parameters in the strategy @@ -475,7 +475,7 @@ creating trades on the exchange. "db_url": "sqlite:///tradesv3.dryrun.sqlite", ``` -3. Remove your Exchange API key and secrete (change them by empty values or fake credentials): +3. Remove your Exchange API key and secret (change them by empty values or fake credentials): ```json "exchange": { From 64da8771617802d77b1d73e8fa90e7d8cc08a500 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Nov 2019 14:24:14 +0100 Subject: [PATCH 317/319] Update stake_amount description --- docs/configuration.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 76bfe8339..a327ae343 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -42,7 +42,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi |----------|-------------| | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). -| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. [Strategy Override](#parameters-in-the-strategy). +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy). | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).* | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). @@ -126,15 +126,19 @@ Values set in the configuration file always overwrite values set in the strategy ### Understand stake_amount The `stake_amount` configuration parameter is an amount of crypto-currency your bot will use for each trade. -The minimal value is 0.0005. If there is not enough crypto-currency in -the account an exception is generated. + +The minimal configuration value is 0.0001. Please check your exchange's trading minimums to avoid problems. + +This setting works in combination with `max_open_trades`. The maximum capital engaged in trades is `stake_amount * max_open_trades`. +For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`. + To allow the bot to trade all the available `stake_currency` in your account set ```json "stake_amount" : "unlimited", ``` -In this case a trade amount is calclulated as: +In this case a trade amount is calculated as: ```python currency_balance / (max_open_trades - current_open_trades) From 111f018c85e98f777f27cad3eb80b3d322155114 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Nov 2019 14:46:09 +0100 Subject: [PATCH 318/319] Add datatype to configuration documentation --- docs/configuration.md | 125 +++++++++++++++++++++-------------------- freqtrade/constants.py | 9 +-- 2 files changed, 70 insertions(+), 64 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a327ae343..2d0764f0c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,67 +40,72 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Command | Description | |----------|-------------| -| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) -| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). -| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy). -| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).* -| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). -| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). -| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
    *Defaults to `true`.* -| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. -| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* -| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). -| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `0.0` (no offset).* -| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* -| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. -| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. -| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). -| `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids. -| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.* -| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book.
    *Defaults to `false`.* -| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.* -| `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks. -| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
    *Defaults to `1`.* -| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
    *Defaults to `1`.* -| `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `true`.* -| `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* -| `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.* -| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). -| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). -| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). -| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. -| `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.** -| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.** -| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secret, do not disclose publicly.** -| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). -| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). -| `exchange.ccxt_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) -| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) -| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
    *Defaults to `60` minutes.* +| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
    ***Datatype:*** *Positive integer (-1 to use `"unlimited"` trades).* +| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *String* +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Positive number or `"unlimited"`.* +| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).*
    ***Datatype:*** *Positive number as ratio.* +| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *String* +| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
    ***Datatype:*** *String* +| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
    *Defaults to `true`.*
    ***Datatype:*** *Boolean* +| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
    ***Datatype:*** *Float* +| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.*
    ***Datatype:*** *Boolean* +| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Dict* +| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Float (as ratio)* +| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Boolean* +| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Float* +| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `0.0` (no offset).*
    ***Datatype:*** *Float* +| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.*
    ***Datatype:*** *Boolean* +| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
    ***Datatype:*** *Integer* +| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
    ***Datatype:*** *Integer* +| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). +| `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids.
    ***Datatype:*** *Boolean* +| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.*
    ***Datatype:*** *Positive Integer* +| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book.
    *Defaults to `false`.*
    ***Datatype:*** *Boolean* +| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.*
    ***Datatype:*** *Float (as ratio)* +| `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks.
    ***Datatype:*** *Boolean* +| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
    *Defaults to `1`.*
    ***Datatype:*** *Positive Integer* +| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
    *Defaults to `1`.*
    ***Datatype:*** *Positive Integer* +| `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `true`.*
    ***Datatype:*** *Boolean* +| `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.*
    ***Datatype:*** *Boolean* +| `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.*
    ***Datatype:*** *Boolean* +| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Dict* +| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Dict* +| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
    ***Datatype:*** *String* +| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
    ***Datatype:*** *Boolean* +| `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* +| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* +| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* +| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)).
    ***Datatype:*** *List* +| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)).
    ***Datatype:*** *List* +| `exchange.ccxt_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
    ***Datatype:*** *Dict* +| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
    ***Datatype:*** *Dict* +| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
    *Defaults to `60` minutes.*
    ***Datatype:*** *Integer* | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. -| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
    *Defaults to `true`.* -| `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
    *Defaults to `StaticPairList`.* -| `telegram.enabled` | Enable the usage of Telegram. -| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.** -| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.** -| `webhook.enabled` | Enable usage of Webhook notifications -| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. -| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. -| `initial_state` | Defines the initial application state. More information below.
    *Defaults to `stopped`.* -| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. -| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. -| `strategy_path` | Adds an additional strategy lookup path (must be a directory). -| `internals.process_throttle_secs` | Set the process throttle. Value in second.
    *Defaults to `5` seconds.* -| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages.
    *Defaults to `60` seconds.* -| `internals.sd_notify` | 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` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. -| `user_data_dir` | Directory containing user data.
    *Defaults to `./user_data/`*. +| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
    *Defaults to `true`.*
    ***Datatype:*** *Boolean* +| `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
    *Defaults to `StaticPairList`.*
    ***Datatype:*** *List of Dicts* +| `telegram.enabled` | Enable the usage of Telegram.
    ***Datatype:*** *Boolean* +| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* +| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* +| `webhook.enabled` | Enable usage of Webhook notifications
    ***Datatype:*** *Boolean* +| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
    ***Datatype:*** *Boolean* +| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
    ***Datatype:*** *String* +| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
    ***Datatype:*** *String* +| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
    ***Datatype:*** *String* +| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details.
    ***Datatype:*** *Boolean* +| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details.
    ***Datatype:*** *IPv4* +| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
    ***Datatype:*** *Integer between 1024 and 65535* +| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* +| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* +| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
    ***Datatype:*** *String, SQLAlchemy connect string* +| `initial_state` | Defines the initial application state. More information below.
    *Defaults to `stopped`.*
    ***Datatype:*** *Enum, either `stopped` or `running`* +| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below.
    ***Datatype:*** *Boolean* +| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`.
    ***Datatype:*** *ClassName* +| `strategy_path` | Adds an additional strategy lookup path (must be a directory).
    ***Datatype:*** *String* +| `internals.process_throttle_secs` | Set the process throttle. Value in second.
    *Defaults to `5` seconds.*
    ***Datatype:*** *Integer* +| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages.
    *Defaults to `60` seconds.*
    ***Datatype:*** *Integer* +| `internals.sd_notify` | 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.
    ***Datatype:*** *Boolean* +| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file.
    ***Datatype:*** *String* +| `user_data_dir` | Directory containing user data.
    *Defaults to `./user_data/`*.
    ***Datatype:*** *String* ### Parameters in the strategy diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0d52bf405..f5e5969eb 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -106,7 +106,7 @@ CONF_SCHEMA = { 'maximum': 1, 'exclusiveMaximum': False, 'use_order_book': {'type': 'boolean'}, - 'order_book_top': {'type': 'number', 'maximum': 20, 'minimum': 1}, + 'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1}, 'check_depth_of_market': { 'type': 'object', 'properties': { @@ -122,8 +122,8 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'use_order_book': {'type': 'boolean'}, - 'order_book_min': {'type': 'number', 'minimum': 1}, - 'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}, + 'order_book_min': {'type': 'integer', 'minimum': 1}, + 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} @@ -210,7 +210,7 @@ CONF_SCHEMA = { 'internals': { 'type': 'object', 'properties': { - 'process_throttle_secs': {'type': 'number'}, + 'process_throttle_secs': {'type': 'integer'}, 'interval': {'type': 'integer'}, 'sd_notify': {'type': 'boolean'}, } @@ -278,5 +278,6 @@ CONF_SCHEMA = { 'bid_strategy', 'unfilledtimeout', 'stoploss', + 'minimal_roi', ] } From 997c4262283e1a413fc9139e0e6a71650145224b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Nov 2019 16:51:03 +0100 Subject: [PATCH 319/319] fix some datatypes --- docs/configuration.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 2d0764f0c..024760fb9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,10 +40,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Command | Description | |----------|-------------| -| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
    ***Datatype:*** *Positive integer (-1 to use `"unlimited"` trades).* +| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
    ***Datatype:*** *Positive integer or -1.* | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *String* -| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Positive number or `"unlimited"`.* -| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).*
    ***Datatype:*** *Positive number as ratio.* +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *Positive float or `"unlimited"`.* +| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
    *Defaults to `0.05` (5%).*
    ***Datatype:*** *Positive Float as ratio.* | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
    ***Datatype:*** *String* | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
    ***Datatype:*** *String* | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
    *Defaults to `true`.*
    ***Datatype:*** *Boolean* @@ -79,7 +79,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)).
    ***Datatype:*** *List* | `exchange.ccxt_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
    ***Datatype:*** *Dict* | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
    ***Datatype:*** *Dict* -| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
    *Defaults to `60` minutes.*
    ***Datatype:*** *Integer* +| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
    *Defaults to `60` minutes.*
    ***Datatype:*** *Positive Integer* | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
    *Defaults to `true`.*
    ***Datatype:*** *Boolean* | `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
    *Defaults to `StaticPairList`.*
    ***Datatype:*** *List of Dicts* @@ -87,7 +87,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.**
    ***Datatype:*** *String* | `webhook.enabled` | Enable usage of Webhook notifications
    ***Datatype:*** *Boolean* -| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
    ***Datatype:*** *Boolean* +| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
    ***Datatype:*** *String* | `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
    ***Datatype:*** *String* | `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
    ***Datatype:*** *String* | `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
    ***Datatype:*** *String* @@ -101,8 +101,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below.
    ***Datatype:*** *Boolean* | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`.
    ***Datatype:*** *ClassName* | `strategy_path` | Adds an additional strategy lookup path (must be a directory).
    ***Datatype:*** *String* -| `internals.process_throttle_secs` | Set the process throttle. Value in second.
    *Defaults to `5` seconds.*
    ***Datatype:*** *Integer* -| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages.
    *Defaults to `60` seconds.*
    ***Datatype:*** *Integer* +| `internals.process_throttle_secs` | Set the process throttle. Value in second.
    *Defaults to `5` seconds.*
    ***Datatype:*** *Positive Integer* +| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages.
    *Defaults to `60` seconds.*
    ***Datatype:*** *Positive Integer or 0* | `internals.sd_notify` | 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.
    ***Datatype:*** *Boolean* | `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file.
    ***Datatype:*** *String* | `user_data_dir` | Directory containing user data.
    *Defaults to `./user_data/`*.
    ***Datatype:*** *String*

    d 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