From fb88953be330d3e7bbb0d8f15de39087e4737096 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 29 May 2019 21:57:14 +0300 Subject: [PATCH 01/92] refactoring download_backtest_data.py --- freqtrade/arguments.py | 47 +++++++----- freqtrade/configuration.py | 31 +++++--- freqtrade/tests/test_arguments.py | 8 +-- scripts/download_backtest_data.py | 116 +++++++++++++++++------------- 4 files changed, 119 insertions(+), 83 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index ddc0dc489..c94c02a8b 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -47,7 +47,7 @@ class Arguments(object): return self.parsed_arg - def parse_args(self) -> argparse.Namespace: + def parse_args(self, no_default_config: bool = False) -> argparse.Namespace: """ Parses given arguments and returns an argparse Namespace instance. """ @@ -55,7 +55,7 @@ class Arguments(object): # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) - if parsed_arg.config is None: + if parsed_arg.config is None and not no_default_config: parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg @@ -427,26 +427,24 @@ class Arguments(object): default=None ) - def testdata_dl_options(self) -> None: + def download_data_options(self) -> None: """ Parses given arguments for testdata download """ self.parser.add_argument( - '--pairs-file', - help='File containing a list of pairs to download.', - dest='pairs_file', - default=None, - metavar='PATH', + '-v', '--verbose', + help='Verbose mode (-vv for more, -vvv to get all messages).', + action='count', + dest='loglevel', + default=0, ) - self.parser.add_argument( - '--export', - help='Export files to given dir.', - dest='export', - default=None, - metavar='PATH', + '--logfile', + help='Log to the file specified', + dest='logfile', + type=str, + metavar='FILE' ) - self.parser.add_argument( '-c', '--config', help='Specify configuration file (default: %(default)s). ' @@ -456,7 +454,21 @@ class Arguments(object): type=str, metavar='PATH', ) - + self.parser.add_argument( + '-d', '--datadir', + help='Path to backtest data.', + dest='datadir', + default=None, + type=str, + metavar='PATH', + ) + self.parser.add_argument( + '--pairs-file', + help='File containing a list of pairs to download.', + dest='pairs_file', + default=None, + metavar='PATH', + ) self.parser.add_argument( '--days', help='Download data for given number of days.', @@ -465,7 +477,6 @@ class Arguments(object): metavar='INT', default=None ) - self.parser.add_argument( '--exchange', help='Exchange name (default: %(default)s). Only valid if no config is provided.', @@ -473,7 +484,6 @@ class Arguments(object): type=str, default='bittrex' ) - self.parser.add_argument( '-t', '--timeframes', help='Specify which tickers to download. Space separated list. \ @@ -484,7 +494,6 @@ class Arguments(object): nargs='+', dest='timeframes', ) - self.parser.add_argument( '--erase', help='Clean all existing data for the selected exchange/pairs/timeframes.', diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index c19580c36..58fd1d6d9 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -122,12 +122,11 @@ class Configuration(object): return conf - def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_logging_config(self, config: Dict[str, Any]) -> None: """ - Extract information for sys.argv and load common configuration - :return: configuration as dictionary + Extract information for sys.argv and load logging configuration: + the --loglevel, --logfile options """ - # Log level if 'loglevel' in self.args and self.args.loglevel: config.update({'verbosity': self.args.loglevel}) @@ -153,6 +152,13 @@ class Configuration(object): set_loggers(config['verbosity']) logger.info('Verbosity set to %s', config['verbosity']) + def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Extract information for sys.argv and load common configuration + :return: configuration as dictionary + """ + self._load_logging_config(config) + # Support for sd_notify if self.args.sd_notify: config['internals'].update({'sd_notify': True}) @@ -228,6 +234,17 @@ class Configuration(object): else: logger.info(logstring.format(config[argname])) + def _load_datadir_config(self, config: Dict[str, Any]) -> None: + """ + Extract information for sys.argv and load datadir configuration: + the --datadir option + """ + if 'datadir' in self.args and self.args.datadir: + config.update({'datadir': self._create_datadir(config, self.args.datadir)}) + else: + config.update({'datadir': self._create_datadir(config, None)}) + logger.info('Using data folder: %s ...', config.get('datadir')) + def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ Extract information for sys.argv and load Optimize configuration @@ -263,11 +280,7 @@ class Configuration(object): self._args_to_config(config, argname='timerange', logstring='Parameter --timerange detected: {} ...') - if 'datadir' in self.args and self.args.datadir: - config.update({'datadir': self._create_datadir(config, self.args.datadir)}) - else: - config.update({'datadir': self._create_datadir(config, None)}) - logger.info('Using data folder: %s ...', config.get('datadir')) + self._load_datadir_config(config) self._args_to_config(config, argname='refresh_pairs', logstring='Parameter -r/--refresh-pairs-cached detected ...') diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index ecd108b5e..afa42f287 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -170,18 +170,18 @@ def test_parse_args_hyperopt_custom() -> None: assert call_args.func is not None -def test_testdata_dl_options() -> None: +def test_download_data_options() -> None: args = [ '--pairs-file', 'file_with_pairs', - '--export', 'export/folder', + '--datadir', 'datadir/folder', '--days', '30', '--exchange', 'binance' ] arguments = Arguments(args, '') - arguments.testdata_dl_options() + arguments.download_data_options() args = arguments.parse_args() assert args.pairs_file == 'file_with_pairs' - assert args.export == 'export/folder' + assert args.datadir == 'datadir/folder' assert args.days == 30 assert args.exchange == 'binance' diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 42b305778..acf86d0d8 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -1,39 +1,39 @@ #!/usr/bin/env python3 """ -This script generates json data +This script generates json files with pairs history data """ +import arrow import json import sys from pathlib import Path -import arrow -from typing import Any, Dict +from typing import Any, Dict, List -from freqtrade.arguments import Arguments -from freqtrade.arguments import TimeRange -from freqtrade.exchange import Exchange +from freqtrade.arguments import Arguments, TimeRange +from freqtrade.configuration import Configuration from freqtrade.data.history import download_pair_history -from freqtrade.configuration import Configuration, set_loggers +from freqtrade.exchange import Exchange from freqtrade.misc import deep_merge_dicts import logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -) -set_loggers(0) + +logger = logging.getLogger('download_backtest_data') DEFAULT_DL_PATH = 'user_data/data' arguments = Arguments(sys.argv[1:], 'download utility') -arguments.testdata_dl_options() -args = arguments.parse_args() +arguments.download_data_options() + +# Do not read the default config if config is not specified +# in the command line options explicitely +args = arguments.parse_args(no_default_config=True) timeframes = args.timeframes +pairs: List = [] + +configuration = Configuration(args) +config: Dict[str, Any] = {} if args.config: - configuration = Configuration(args) - - config: Dict[str, Any] = {} # Now expecting a list of config filenames here, not a string for path in args.config: print(f"Using config: {path}...") @@ -42,9 +42,19 @@ if args.config: config['stake_currency'] = '' # Ensure we do not use Exchange credentials + config['exchange']['dry_run'] = True config['exchange']['key'] = '' config['exchange']['secret'] = '' + + if args.exchange: + config['exchange']['name'] = args.exchange + + pairs = config['exchange']['pair_whitelist'] + timeframes = [config['ticker_interval']] + else: + if not args.exchange: + sys.exit("No exchange specified.") config = { 'stake_currency': '', 'dry_run': True, @@ -60,55 +70,59 @@ else: } } +configuration._load_logging_config(config) +configuration._load_datadir_config(config) -dl_path = Path(DEFAULT_DL_PATH).joinpath(config['exchange']['name']) -if args.export: - dl_path = Path(args.export) - -if not dl_path.is_dir(): - sys.exit(f'Directory {dl_path} does not exist.') +dl_path = Path(config['datadir']) pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json') -if not pairs_file.exists(): - sys.exit(f'No pairs file found with path {pairs_file}.') -with pairs_file.open() as file: - PAIRS = list(set(json.load(file))) +if not pairs or args.pairs_file: + print(f'Reading pairs file "{pairs_file}".') + # Download pairs from the pairs file if no config is specified + # or if pairs file is specified explicitely + if not pairs_file.exists(): + sys.exit(f'No pairs file found with path "{pairs_file}".') -PAIRS.sort() + with pairs_file.open() as file: + pairs = list(set(json.load(file))) + pairs.sort() timerange = TimeRange() if args.days: time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") timerange = arguments.parse_timerange(f'{time_since}-') +print(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') -print(f'About to download pairs: {PAIRS} to {dl_path}') - -# Init exchange -exchange = Exchange(config) pairs_not_available = [] -for pair in PAIRS: - if pair not in exchange._api.markets: - pairs_not_available.append(pair) - print(f"skipping pair {pair}") - continue - for ticker_interval in timeframes: - pair_print = pair.replace('/', '_') - filename = f'{pair_print}-{ticker_interval}.json' - dl_file = dl_path.joinpath(filename) - if args.erase and dl_file.exists(): - print(f'Deleting existing data for pair {pair}, interval {ticker_interval}') - dl_file.unlink() +try: + # Init exchange + exchange = Exchange(config) - print(f'downloading pair {pair}, interval {ticker_interval}') - download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, - ticker_interval=ticker_interval, - timerange=timerange) + for pair in pairs: + if pair not in exchange._api.markets: + pairs_not_available.append(pair) + print(f"skipping pair {pair}") + continue + for ticker_interval in timeframes: + pair_print = pair.replace('/', '_') + filename = f'{pair_print}-{ticker_interval}.json' + dl_file = dl_path.joinpath(filename) + if args.erase and dl_file.exists(): + print(f'Deleting existing data for pair {pair}, interval {ticker_interval}') + dl_file.unlink() + print(f'downloading pair {pair}, interval {ticker_interval}') + download_pair_history(datadir=dl_path, exchange=exchange, + pair=pair, ticker_interval=ticker_interval, + timerange=timerange) -if pairs_not_available: - print(f"Pairs [{','.join(pairs_not_available)}] not availble.") +except KeyboardInterrupt: + sys.exit("SIGINT received, aborting ...") + +finally: + if pairs_not_available: + print(f"Pairs [{','.join(pairs_not_available)}] not availble.") From f463817c883a8510054bbc486c5c18bfe8da9eab Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 09:51:32 +0300 Subject: [PATCH 02/92] change metavar for --pairs-file --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index c94c02a8b..711975fd0 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -467,7 +467,7 @@ class Arguments(object): help='File containing a list of pairs to download.', dest='pairs_file', default=None, - metavar='PATH', + metavar='FILE', ) self.parser.add_argument( '--days', From 11f535e79f74d0fd41632768c65773595356c75d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 09:54:58 +0300 Subject: [PATCH 03/92] change prints to logging --- scripts/download_backtest_data.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index acf86d0d8..acdfb25ac 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -36,7 +36,7 @@ config: Dict[str, Any] = {} if args.config: # Now expecting a list of config filenames here, not a string for path in args.config: - print(f"Using config: {path}...") + logger.info(f"Using config: {path}...") # Merge config options, overwriting old values config = deep_merge_dicts(configuration._load_config_file(path), config) @@ -78,7 +78,7 @@ dl_path = Path(config['datadir']) pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json') if not pairs or args.pairs_file: - print(f'Reading pairs file "{pairs_file}".') + logger.info(f'Reading pairs file "{pairs_file}".') # Download pairs from the pairs file if no config is specified # or if pairs file is specified explicitely if not pairs_file.exists(): @@ -94,7 +94,7 @@ if args.days: time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") timerange = arguments.parse_timerange(f'{time_since}-') -print(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') +logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') pairs_not_available = [] @@ -105,17 +105,17 @@ try: for pair in pairs: if pair not in exchange._api.markets: pairs_not_available.append(pair) - print(f"skipping pair {pair}") + logger.info(f"skipping pair {pair}") continue for ticker_interval in timeframes: pair_print = pair.replace('/', '_') filename = f'{pair_print}-{ticker_interval}.json' dl_file = dl_path.joinpath(filename) if args.erase and dl_file.exists(): - print(f'Deleting existing data for pair {pair}, interval {ticker_interval}') + logger.info(f'Deleting existing data for pair {pair}, interval {ticker_interval}') dl_file.unlink() - print(f'downloading pair {pair}, interval {ticker_interval}') + logger.info(f'downloading pair {pair}, interval {ticker_interval}') download_pair_history(datadir=dl_path, exchange=exchange, pair=pair, ticker_interval=ticker_interval, timerange=timerange) @@ -125,4 +125,4 @@ except KeyboardInterrupt: finally: if pairs_not_available: - print(f"Pairs [{','.join(pairs_not_available)}] not availble.") + logger.info(f"Pairs [{','.join(pairs_not_available)}] not availble.") From 39932627bd25d5fde92242a6e307574846e372c4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 11:03:17 +0300 Subject: [PATCH 04/92] typo in log message fixed --- scripts/download_backtest_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index acdfb25ac..b7c302437 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -125,4 +125,4 @@ except KeyboardInterrupt: finally: if pairs_not_available: - logger.info(f"Pairs [{','.join(pairs_not_available)}] not availble.") + logger.info(f"Pairs [{','.join(pairs_not_available)}] not available.") From ef15f2bdc67c65ec2d05f730006502f536a8cde4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 11:07:31 +0300 Subject: [PATCH 05/92] log messages slightly improved --- scripts/download_backtest_data.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index b7c302437..196af5bed 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -105,17 +105,18 @@ try: for pair in pairs: if pair not in exchange._api.markets: pairs_not_available.append(pair) - logger.info(f"skipping pair {pair}") + logger.info(f"Skipping pair {pair}...") continue for ticker_interval in timeframes: pair_print = pair.replace('/', '_') filename = f'{pair_print}-{ticker_interval}.json' dl_file = dl_path.joinpath(filename) if args.erase and dl_file.exists(): - logger.info(f'Deleting existing data for pair {pair}, interval {ticker_interval}') + logger.info( + f'Deleting existing data for pair {pair}, interval {ticker_interval}.') dl_file.unlink() - logger.info(f'downloading pair {pair}, interval {ticker_interval}') + logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') download_pair_history(datadir=dl_path, exchange=exchange, pair=pair, ticker_interval=ticker_interval, timerange=timerange) @@ -125,4 +126,6 @@ except KeyboardInterrupt: finally: if pairs_not_available: - logger.info(f"Pairs [{','.join(pairs_not_available)}] not available.") + logger.info( + f"Pairs [{','.join(pairs_not_available)}] not available " + f"on exchange {config['exchange']['name']}.") From 1add432673aa76c378bed6030ce0f53939353443 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 23:00:19 +0300 Subject: [PATCH 06/92] docs adjusted --- docs/backtesting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index a25d3c1d5..5a25bc255 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -123,11 +123,12 @@ python scripts/download_backtest_data.py --exchange binance This will download ticker data for all the currency pairs you defined in `pairs.json`. -- To use a different folder than the exchange specific default, use `--export user_data/data/some_directory`. +- To use a different folder than the exchange specific default, use `--datadir user_data/data/some_directory`. - To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`. - To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`. - To download ticker data for only 10 days, use `--days 10`. - Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. +- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options. For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands). From dc0326db2737ba1bc213bab7f76bbffcc922c810 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:09:30 +0300 Subject: [PATCH 07/92] fix handling --exchange --- freqtrade/arguments.py | 8 +------- scripts/download_backtest_data.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 711975fd0..0d4288ef3 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -443,7 +443,7 @@ class Arguments(object): help='Log to the file specified', dest='logfile', type=str, - metavar='FILE' + metavar='FILE', ) self.parser.add_argument( '-c', '--config', @@ -458,15 +458,12 @@ class Arguments(object): '-d', '--datadir', help='Path to backtest data.', dest='datadir', - default=None, - type=str, metavar='PATH', ) self.parser.add_argument( '--pairs-file', help='File containing a list of pairs to download.', dest='pairs_file', - default=None, metavar='FILE', ) self.parser.add_argument( @@ -475,14 +472,11 @@ class Arguments(object): dest='days', type=int, metavar='INT', - default=None ) self.parser.add_argument( '--exchange', help='Exchange name (default: %(default)s). Only valid if no config is provided.', dest='exchange', - type=str, - default='bittrex' ) self.parser.add_argument( '-t', '--timeframes', diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 196af5bed..278879fb0 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -27,6 +27,9 @@ arguments.download_data_options() # in the command line options explicitely args = arguments.parse_args(no_default_config=True) +# Use bittrex as default exchange +exchange_name = args.exchange or 'bittrex' + timeframes = args.timeframes pairs: List = [] @@ -46,20 +49,15 @@ if args.config: config['exchange']['key'] = '' config['exchange']['secret'] = '' - if args.exchange: - config['exchange']['name'] = args.exchange - pairs = config['exchange']['pair_whitelist'] timeframes = [config['ticker_interval']] else: - if not args.exchange: - sys.exit("No exchange specified.") config = { 'stake_currency': '', 'dry_run': True, 'exchange': { - 'name': args.exchange, + 'name': exchange_name, 'key': '', 'secret': '', 'pair_whitelist': [], @@ -71,6 +69,13 @@ else: } configuration._load_logging_config(config) + +if args.config and args.exchange: + logger.warning("The --exchange option is ignored, using exchange settings from the configuration file.") + +# Check if the exchange set by the user is supported +configuration.check_exchange(config) + configuration._load_datadir_config(config) dl_path = Path(config['datadir']) From cd60d6d99adc61ca030f418d3a60de9bfbd43fd1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:10:21 +0300 Subject: [PATCH 08/92] make --days positive int only --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 0d4288ef3..09fea5e63 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -470,7 +470,7 @@ class Arguments(object): '--days', help='Download data for given number of days.', dest='days', - type=int, + type=Arguments.check_int_positive, metavar='INT', ) self.parser.add_argument( From d55f2be942b5f9a58cd085e815c5a11311f802c0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:21:59 +0300 Subject: [PATCH 09/92] make flake happy --- scripts/download_backtest_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 278879fb0..76b2c415b 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -71,7 +71,8 @@ else: configuration._load_logging_config(config) if args.config and args.exchange: - logger.warning("The --exchange option is ignored, using exchange settings from the configuration file.") + logger.warning("The --exchange option is ignored, " + "using exchange settings from the configuration file.") # Check if the exchange set by the user is supported configuration.check_exchange(config) From 4801af4c77afbb10a8ec756e6c22955521b08a17 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:42:14 +0300 Subject: [PATCH 10/92] debug logging for IStrategy.advise_*() added --- freqtrade/strategy/interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index db266d95f..4e2d96c55 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -378,6 +378,7 @@ class IStrategy(ABC): :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ + logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) @@ -393,6 +394,7 @@ class IStrategy(ABC): :param pair: Additional information, like the currently traded pair :return: DataFrame with buy column """ + logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.") if self._buy_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) @@ -408,6 +410,7 @@ class IStrategy(ABC): :param pair: Additional information, like the currently traded pair :return: DataFrame with sell column """ + logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.") if self._sell_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) From 7322a34fa426d0eb42f7ad5711b997ad356c19fa Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:58:19 +0300 Subject: [PATCH 11/92] fix metadata in tests --- freqtrade/tests/strategy/test_strategy.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 2ed2567f9..b96f9c79e 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -63,15 +63,14 @@ def test_search_strategy(): def test_load_strategy(result): resolver = StrategyResolver({'strategy': 'TestStrategy'}) - metadata = {'pair': 'ETH/BTC'} - assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata) + assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_byte64(result): with open("freqtrade/tests/strategy/test_strategy.py", "r") as file: encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8") resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) - assert 'adx' in resolver.strategy.advise_indicators(result, 'ETH/BTC') + assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_invalid_directory(result, caplog): @@ -371,7 +370,7 @@ def test_deprecate_populate_indicators(result): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC') + indicators = resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -380,7 +379,7 @@ def test_deprecate_populate_indicators(result): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - resolver.strategy.advise_buy(indicators, 'ETH/BTC') + resolver.strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -389,7 +388,7 @@ def test_deprecate_populate_indicators(result): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - resolver.strategy.advise_sell(indicators, 'ETH_BTC') + resolver.strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ From 676e7300132e15aa55a8cdb844e2f3b582140184 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 13:18:35 +0300 Subject: [PATCH 12/92] enhance check_exchange --- freqtrade/configuration.py | 41 +++++++++++++++++++++------ freqtrade/exchange/__init__.py | 6 ++-- freqtrade/exchange/exchange.py | 18 ++++++++---- freqtrade/tests/test_configuration.py | 2 +- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index c19580c36..90393cae2 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -13,7 +13,8 @@ from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants -from freqtrade.exchange import is_exchange_supported, supported_exchanges +from freqtrade.exchange import (is_exchange_bad, is_exchange_known, + is_exchange_officially_supported, known_exchanges) from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode @@ -375,22 +376,44 @@ class Configuration(object): return self.config - def check_exchange(self, config: Dict[str, Any]) -> bool: + def check_exchange(self, config: Dict[str, Any], check_for_bad: bool = True) -> bool: """ Check if the exchange name in the config file is supported by Freqtrade - :return: True or raised an exception if the exchange if not supported + :param check_for_bad: if True, check the exchange against the list of known 'bad' + exchanges + :return: False if exchange is 'bad', i.e. is known to work with the bot with + critical issues or does not work at all, crashes, etc. True otherwise. + raises an exception if the exchange if not supported by ccxt + and thus is not known for the Freqtrade at all. """ - exchange = config.get('exchange', {}).get('name').lower() - if not is_exchange_supported(exchange): + logger.info("Checking exchange...") - exception_msg = f'Exchange "{exchange}" not supported.\n' \ - f'The following exchanges are supported: ' \ - f'{", ".join(supported_exchanges())}' + exchange = config.get('exchange', {}).get('name').lower() + if not is_exchange_known(exchange): + exception_msg = f'Exchange "{exchange}" is not supported by ccxt ' \ + f'and not known for the bot.\n' \ + f'The following exchanges are supported by ccxt: ' \ + f'{", ".join(known_exchanges())}' logger.critical(exception_msg) raise OperationalException( exception_msg ) - logger.debug('Exchange "%s" supported', exchange) + logger.info(f'Exchange "{exchange}" is supported by ccxt and known for the bot.') + + if is_exchange_officially_supported(exchange): + logger.info(f'Exchange "{exchange}" is officially supported ' + f'by the Freqtrade development team.') + else: + logger.warning(f'Exchange "{exchange}" is not officially supported ' + f'by the Freqtrade development team. ' + f'It may work with serious issues or not work at all. ' + f'Use it at your own discretion.') + + if check_for_bad and is_exchange_bad(exchange): + logger.warning(f'Exchange "{exchange}" is known to not work with Freqtrade yet. ' + f'Use it only for development and testing purposes.') + return False + return True diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 3c90e69ee..29e50e29c 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,6 +1,8 @@ from freqtrade.exchange.exchange import Exchange # noqa: F401 -from freqtrade.exchange.exchange import (is_exchange_supported, # noqa: F401 - supported_exchanges) +from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 + is_exchange_known, + is_exchange_officially_supported, + known_exchanges) from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_minutes, timeframe_to_msecs) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea6996efb..1196f5efe 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -156,8 +156,8 @@ class Exchange(object): # Find matching class for the given exchange name name = exchange_config['name'] - if not is_exchange_supported(name, ccxt_module): - raise OperationalException(f'Exchange {name} is not supported') +# if not is_exchange_supported(name, ccxt_module): +# raise OperationalException(f'Exchange {name} is not supported') ex_config = { 'apiKey': exchange_config.get('key'), @@ -722,11 +722,19 @@ class Exchange(object): raise OperationalException(e) -def is_exchange_supported(exchange: str, ccxt_module=None) -> bool: - return exchange in supported_exchanges(ccxt_module) +def is_exchange_bad(exchange: str) -> bool: + return exchange in ['bitmex'] -def supported_exchanges(ccxt_module=None) -> List[str]: +def is_exchange_known(exchange: str, ccxt_module=None) -> bool: + return exchange in known_exchanges(ccxt_module) + + +def is_exchange_officially_supported(exchange: str) -> bool: + return exchange in ['bittrex', 'binance'] + + +def known_exchanges(ccxt_module=None) -> List[str]: return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index aee0dfadd..03f0c004c 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -484,7 +484,7 @@ def test_check_exchange(default_conf, caplog) -> None: with pytest.raises( OperationalException, - match=r'.*Exchange "unknown_exchange" not supported.*' + match=r'.*Exchange "unknown_exchange" is not supported by ccxt and not known for the bot.*' ): configuration.check_exchange(default_conf) From db6ccef6bdad38d3438bc08554ee91d500a514c1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 13:43:29 +0300 Subject: [PATCH 13/92] return back check in init_ccxt() --- freqtrade/exchange/exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 1196f5efe..0d4ed364d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -156,8 +156,8 @@ class Exchange(object): # Find matching class for the given exchange name name = exchange_config['name'] -# if not is_exchange_supported(name, ccxt_module): -# raise OperationalException(f'Exchange {name} is not supported') + if not is_exchange_known(name, ccxt_module): + raise OperationalException(f'Exchange {name} is not supported by ccxt') ex_config = { 'apiKey': exchange_config.get('key'), From dc7f8837518e280ee950b811c9d2ee8e7826d810 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 13:47:04 +0300 Subject: [PATCH 14/92] no need to duplicate this long error message --- freqtrade/configuration.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 90393cae2..bd4717894 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -390,14 +390,11 @@ class Configuration(object): exchange = config.get('exchange', {}).get('name').lower() if not is_exchange_known(exchange): - exception_msg = f'Exchange "{exchange}" is not supported by ccxt ' \ - f'and not known for the bot.\n' \ - f'The following exchanges are supported by ccxt: ' \ - f'{", ".join(known_exchanges())}' - - logger.critical(exception_msg) raise OperationalException( - exception_msg + f'Exchange "{exchange}" is not supported by ccxt ' + f'and not known for the bot.\n' + f'The following exchanges are supported by ccxt: ' + f'{", ".join(known_exchanges())}' ) logger.info(f'Exchange "{exchange}" is supported by ccxt and known for the bot.') From 9c64965808d02fc8e100a48e76f34022858f9a9e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 12 Jun 2019 12:33:20 +0300 Subject: [PATCH 15/92] list-exchanges subcommand added --- freqtrade/arguments.py | 18 +++++++++++++++++ freqtrade/utils.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 freqtrade/utils.py diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 89b587c6f..101d927bf 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -335,12 +335,25 @@ class Arguments(object): metavar='INT', ) + @staticmethod + def list_exchanges_options(parser: argparse.ArgumentParser) -> None: + """ + Parses given arguments for the list-exchanges command. + """ + parser.add_argument( + '-1', + help='Print exchanges in one column', + action='store_true', + dest='print_one_column', + ) + def _build_subcommands(self) -> None: """ Builds and attaches all subcommands :return: None """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge + from freqtrade.utils import start_list_exchanges subparsers = self.parser.add_subparsers(dest='subparser') @@ -362,6 +375,11 @@ class Arguments(object): self.optimizer_shared_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd) + # Add list-exchanges subcommand + list_exchanges_cmd = subparsers.add_parser('list-exchanges', help='List known exchanges.') + list_exchanges_cmd.set_defaults(func=start_list_exchanges) + self.list_exchanges_options(list_exchanges_cmd) + @staticmethod def parse_timerange(text: Optional[str]) -> TimeRange: """ diff --git a/freqtrade/utils.py b/freqtrade/utils.py new file mode 100644 index 000000000..944287001 --- /dev/null +++ b/freqtrade/utils.py @@ -0,0 +1,44 @@ +import logging +from argparse import Namespace +from typing import Any, Dict + +from freqtrade.configuration import Configuration +from freqtrade.exchange import supported_exchanges +from freqtrade.state import RunMode + + +logger = logging.getLogger(__name__) + + +def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: + """ + Prepare the configuration for the Hyperopt module + :param args: Cli args from Arguments() + :return: Configuration + """ + configuration = Configuration(args, method) + config = configuration.load_config() + + # Ensure we do not use Exchange credentials + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + + return config + + +def start_list_exchanges(args: Namespace) -> None: + """ + Start listing known exchanges + :param args: Cli args from Arguments() + :return: None + """ + + # Initialize configuration + config = setup_configuration(args, RunMode.OTHER) + + logger.debug('Starting freqtrade in cli-util mode') + + if args.print_one_column: + print('\n'.join(supported_exchanges())) + else: + print(f"Supported exchanges: {', '.join(supported_exchanges())}") From 8df40a6ff945fbf679ad3484bc8aec93b46d271b Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 12 Jun 2019 22:40:50 +0300 Subject: [PATCH 16/92] make flake happy --- freqtrade/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 944287001..394507059 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -34,7 +34,7 @@ def start_list_exchanges(args: Namespace) -> None: """ # Initialize configuration - config = setup_configuration(args, RunMode.OTHER) + _ = setup_configuration(args, RunMode.OTHER) logger.debug('Starting freqtrade in cli-util mode') From 0cc2210f222d54127ba58b7fdf7173b32fe0746d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 12 Jun 2019 22:37:43 +0300 Subject: [PATCH 17/92] wording fixed --- freqtrade/configuration.py | 20 ++++++++++---------- freqtrade/exchange/__init__.py | 4 ++-- freqtrade/exchange/exchange.py | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index bd4717894..b85345b45 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -13,8 +13,8 @@ from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants -from freqtrade.exchange import (is_exchange_bad, is_exchange_known, - is_exchange_officially_supported, known_exchanges) +from freqtrade.exchange import (is_exchange_bad, is_exchange_available, + is_exchange_officially_supported, available_exchanges) from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode @@ -389,27 +389,27 @@ class Configuration(object): logger.info("Checking exchange...") exchange = config.get('exchange', {}).get('name').lower() - if not is_exchange_known(exchange): + if not is_exchange_available(exchange): raise OperationalException( f'Exchange "{exchange}" is not supported by ccxt ' - f'and not known for the bot.\n' + f'and therefore not available for the bot.\n' f'The following exchanges are supported by ccxt: ' - f'{", ".join(known_exchanges())}' + f'{", ".join(available_exchanges())}' ) - logger.info(f'Exchange "{exchange}" is supported by ccxt and known for the bot.') + logger.info(f'Exchange "{exchange}" is supported by ccxt ' + f'and therefore available for the bot.') if is_exchange_officially_supported(exchange): logger.info(f'Exchange "{exchange}" is officially supported ' f'by the Freqtrade development team.') else: - logger.warning(f'Exchange "{exchange}" is not officially supported ' - f'by the Freqtrade development team. ' - f'It may work with serious issues or not work at all. ' + logger.warning(f'Exchange "{exchange}" is not officially supported. ' + f'It may work flawlessly (please report back) or have serious issues. ' f'Use it at your own discretion.') if check_for_bad and is_exchange_bad(exchange): - logger.warning(f'Exchange "{exchange}" is known to not work with Freqtrade yet. ' + logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. ' f'Use it only for development and testing purposes.') return False diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 29e50e29c..5c58320f6 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,8 +1,8 @@ from freqtrade.exchange.exchange import Exchange # noqa: F401 from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 - is_exchange_known, + is_exchange_available, is_exchange_officially_supported, - known_exchanges) + available_exchanges) from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_minutes, timeframe_to_msecs) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0d4ed364d..194e1d883 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -156,7 +156,7 @@ class Exchange(object): # Find matching class for the given exchange name name = exchange_config['name'] - if not is_exchange_known(name, ccxt_module): + if not is_exchange_available(name, ccxt_module): raise OperationalException(f'Exchange {name} is not supported by ccxt') ex_config = { @@ -726,15 +726,15 @@ def is_exchange_bad(exchange: str) -> bool: return exchange in ['bitmex'] -def is_exchange_known(exchange: str, ccxt_module=None) -> bool: - return exchange in known_exchanges(ccxt_module) +def is_exchange_available(exchange: str, ccxt_module=None) -> bool: + return exchange in available_exchanges(ccxt_module) def is_exchange_officially_supported(exchange: str) -> bool: return exchange in ['bittrex', 'binance'] -def known_exchanges(ccxt_module=None) -> List[str]: +def available_exchanges(ccxt_module=None) -> List[str]: return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges From a65c89f09074c7c2eba664afe6335f3186788b03 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 12 Jun 2019 23:03:16 +0300 Subject: [PATCH 18/92] test adjusted --- freqtrade/tests/test_configuration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 03f0c004c..c8beb3512 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -484,7 +484,8 @@ def test_check_exchange(default_conf, caplog) -> None: with pytest.raises( OperationalException, - match=r'.*Exchange "unknown_exchange" is not supported by ccxt and not known for the bot.*' + match=r'.*Exchange "unknown_exchange" is not supported by ccxt ' + r'and therefore not available for the bot.*' ): configuration.check_exchange(default_conf) From a4d8424268f0d2ed8fdb03d6cc3f16b51b888215 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 19:34:46 +0200 Subject: [PATCH 19/92] trailing_stop_positive should only be set when needed, and none/undefined otherwise --- user_data/strategies/test_strategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 66a5f8c09..1dd6406b4 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -44,8 +44,8 @@ class TestStrategy(IStrategy): # trailing stoploss trailing_stop = False - trailing_stop_positive = 0.01 - trailing_stop_positive_offset = 0.0 # Disabled / not configured + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured # Optimal ticker interval for the strategy ticker_interval = '5m' From b64b6a2583f55f1f1d139a45a59f70bc633c4c61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 19:35:20 +0200 Subject: [PATCH 20/92] Support trailing_stop_positive options in BTContainer --- freqtrade/tests/optimize/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 457113cb7..41500051f 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -29,6 +29,10 @@ class BTContainer(NamedTuple): trades: List[BTrade] profit_perc: float trailing_stop: bool = False + trailing_only_offset_is_reached: bool = False + trailing_stop_positive: float = None + trailing_stop_positive_offset: float = 0.0 + use_sell_signal: bool = False def _get_frame_time_from_offset(offset): From 578180f45b986aaa5dade8425102aee0baa59dbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:00:56 +0200 Subject: [PATCH 21/92] Add test for sell-signal sell --- .../tests/optimize/test_backtest_detail.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 32c6bd09b..35f705e16 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -14,6 +14,21 @@ from freqtrade.tests.optimize import (BTContainer, BTrade, _get_frame_time_from_offset, tests_ticker_interval) +# Test 0 Sell signal sell +# Test with Stop-loss at 1% +# TC0: Sell signal in candle 3 +tc0 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], # exit with stoploss hit + [3, 5010, 5000, 4980, 5010, 6172, 0, 1], + [4, 5010, 4987, 4977, 4995, 6172, 0, 0], + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.01, roi=1, profit_perc=0.002, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] +) + # Test 1 Minus 8% Close # Test with Stop-loss at 1% # TC1: Stop-Loss Triggered 1% loss @@ -146,7 +161,7 @@ tc8 = BTContainer(data=[ # Test 9 - trailing_stop should raise - high and low in same candle. # Candle Data for test 9 # Set stop-loss at 10%, ROI at 10% (should not apply) -# TC9: Trailing stoploss - stoploss should be adjusted candle 2 +# TC9: Trailing stoploss - stoploss should be adjusted candle 3 tc9 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -159,6 +174,7 @@ tc9 = BTContainer(data=[ ) TESTS = [ + tc0, tc1, tc2, tc3, @@ -180,6 +196,13 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: default_conf["minimal_roi"] = {"0": data.roi} default_conf["ticker_interval"] = tests_ticker_interval 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 + if data.trailing_stop_positive: + default_conf["trailing_stop_positive"] = data.trailing_stop_positive + default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset + default_conf["experimental"] = {"use_sell_signal": data.use_sell_signal} + mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0)) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) From 160894c0312cf5c778a3d04b8968f62b28aecd4c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:04:52 +0200 Subject: [PATCH 22/92] Calculate profit_high to make sure stoploss_positive_offset is correct --- freqtrade/strategy/interface.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index db266d95f..8570c354f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -308,14 +308,16 @@ class IStrategy(ABC): if trailing_stop: # trailing stoploss handling - sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) + # Make sure current_profit is calculated using high for backtesting. + high_profit = current_profit if not high else trade.calc_profit_percent(high) + # Don't update stoploss if trailing_only_offset_is_reached is true. - if not (tsl_only_offset and current_profit < sl_offset): + if not (tsl_only_offset and high_profit < sl_offset): # Specific handling for trailing_stop_positive - if 'trailing_stop_positive' in self.config and current_profit > sl_offset: + if 'trailing_stop_positive' in self.config and high_profit > sl_offset: # Ignore mypy error check in configuration that this is a float stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore logger.debug(f"using positive stop loss: {stop_loss_value} " From 550fbad53e7fc64074744945fea988273c0ebad8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:05:49 +0200 Subject: [PATCH 23/92] Add test-cases with trailing_stop_offsets --- .../tests/optimize/test_backtest_detail.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 35f705e16..402e22391 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -173,6 +173,57 @@ tc9 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) +# Test 10 - trailing_stop should raise so candle 3 causes a stoploss +# without applying trailing_stop_positive since stoploss_offset is at 10%. +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC10: Trailing stoploss - stoploss should be adjusted candle 2 +tc10 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=-0.1, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.10, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)] +) + +# Test 11 - trailing_stop should raise so candle 3 causes a stoploss +# applying a positive trailing stop of 3% since stop_positive_offset is reached. +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC11: Trailing stoploss - stoploss should be adjusted candle 2, +tc11 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=0.019, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + +# Test 12 - trailing_stop should raise in candle 2 and cause a stoploss in the same candle +# applying a positive trailing stop of 3% since stop_positive_offset is reached. +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC12: Trailing stoploss - stoploss should be adjusted candle 2, +tc12 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 4650, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=0.019, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] +) + TESTS = [ tc0, tc1, @@ -184,6 +235,9 @@ TESTS = [ tc7, tc8, tc9, + tc10, + tc11, + tc12, ] From e08fda074ad2ea504b830c8760ad1a5e198e9b0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:26:47 +0200 Subject: [PATCH 24/92] Fix bug with timeframe handling --- scripts/download_backtest_data.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 76b2c415b..8493c0872 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -30,7 +30,6 @@ args = arguments.parse_args(no_default_config=True) # Use bittrex as default exchange exchange_name = args.exchange or 'bittrex' -timeframes = args.timeframes pairs: List = [] configuration = Configuration(args) @@ -50,7 +49,9 @@ if args.config: config['exchange']['secret'] = '' pairs = config['exchange']['pair_whitelist'] - timeframes = [config['ticker_interval']] + + # Don't fail if ticker_interval is not in the configuration + timeframes = [config.get('ticker_interval')] else: config = { @@ -68,6 +69,8 @@ else: } } +timeframes = args.timeframes + configuration._load_logging_config(config) if args.config and args.exchange: From 9657b1a17f454fcd81f66f91c2c8949c7495acfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:37:17 +0200 Subject: [PATCH 25/92] explict parse to string for ticker-interval --- scripts/download_backtest_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 8493c0872..a39500d0e 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -127,7 +127,7 @@ try: logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, ticker_interval=ticker_interval, + pair=pair, ticker_interval=str(ticker_interval), timerange=timerange) except KeyboardInterrupt: From 04ea66c97722901caeae519fd0b38feb886bae1e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 02:58:34 +0300 Subject: [PATCH 26/92] fix handling timeframes --- freqtrade/arguments.py | 1 - scripts/download_backtest_data.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 09fea5e63..46fb9e7b3 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -484,7 +484,6 @@ class Arguments(object): Default: %(default)s.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], - default=['1m', '5m'], nargs='+', dest='timeframes', ) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index a39500d0e..6263d0e2f 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -50,8 +50,10 @@ if args.config: pairs = config['exchange']['pair_whitelist'] - # Don't fail if ticker_interval is not in the configuration - timeframes = [config.get('ticker_interval')] + if config.get('ticker_interval'): + timeframes = args.timeframes or [config.get('ticker_interval')] + else: + timeframes = args.timeframes or ['1m', '5m'] else: config = { @@ -68,8 +70,7 @@ else: } } } - -timeframes = args.timeframes + timeframes = args.timeframes or ['1m', '5m'] configuration._load_logging_config(config) From ee113ab8edb29269221fd0a10830c1a5339cafde Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 18:40:02 +0300 Subject: [PATCH 27/92] log messages aligned --- freqtrade/configuration.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index b85345b45..28faacf90 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -397,20 +397,19 @@ class Configuration(object): f'{", ".join(available_exchanges())}' ) - logger.info(f'Exchange "{exchange}" is supported by ccxt ' - f'and therefore available for the bot.') - - if is_exchange_officially_supported(exchange): - logger.info(f'Exchange "{exchange}" is officially supported ' - f'by the Freqtrade development team.') - else: - logger.warning(f'Exchange "{exchange}" is not officially supported. ' - f'It may work flawlessly (please report back) or have serious issues. ' - f'Use it at your own discretion.') - if check_for_bad and is_exchange_bad(exchange): logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. ' f'Use it only for development and testing purposes.') return False + if is_exchange_officially_supported(exchange): + logger.info(f'Exchange "{exchange}" is officially supported ' + f'by the Freqtrade development team.') + else: + logger.warning(f'Exchange "{exchange}" is supported by ccxt ' + f'and therefore available for the bot but not officially supported ' + f'by the Freqtrade development team. ' + f'It may work flawlessly (please report back) or have serious issues. ' + f'Use it at your own discretion.') + return True From 941fb4ebbb3c7f3d53bc3741e20d086b7545b072 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 18:40:25 +0300 Subject: [PATCH 28/92] tests added --- freqtrade/tests/test_configuration.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index c8beb3512..82167125c 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -470,15 +470,27 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_check_exchange(default_conf, caplog) -> None: configuration = Configuration(Namespace()) - # Test a valid exchange + # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'BITTREX'}) assert configuration.check_exchange(default_conf) - # Test a valid exchange + # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'binance'}) assert configuration.check_exchange(default_conf) - # Test a invalid exchange + # Test an available exchange, supported by ccxt + default_conf.get('exchange').update({'name': 'kraken'}) + assert configuration.check_exchange(default_conf) + + # Test a 'bad' exchange, which known to have serious problems + default_conf.get('exchange').update({'name': 'bitmex'}) + assert not configuration.check_exchange(default_conf) + + # Test a 'bad' exchange with check_for_bad=False + default_conf.get('exchange').update({'name': 'bitmex'}) + assert configuration.check_exchange(default_conf, False) + + # Test an invalid exchange default_conf.get('exchange').update({'name': 'unknown_exchange'}) configuration.config = default_conf From cedd38455f0e2da85a25cbae32b6bf11e02a9490 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 21:54:38 +0300 Subject: [PATCH 29/92] remove configuration from list-exchanges --- freqtrade/utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 394507059..45bd9fd3f 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -28,17 +28,13 @@ def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: def start_list_exchanges(args: Namespace) -> None: """ - Start listing known exchanges + Print available exchanges :param args: Cli args from Arguments() :return: None """ - # Initialize configuration - _ = setup_configuration(args, RunMode.OTHER) - - logger.debug('Starting freqtrade in cli-util mode') - if args.print_one_column: print('\n'.join(supported_exchanges())) else: - print(f"Supported exchanges: {', '.join(supported_exchanges())}") + print(f"Exchanges supported by ccxt and available for Freqtrade: " + f"{', '.join(supported_exchanges())}") From 1af988711b9020d78866c6bbdcb065fe58eeeaf2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 21:59:16 +0300 Subject: [PATCH 30/92] add --one-column as an alias option --- freqtrade/arguments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 101d927bf..a8e8af2ad 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -341,7 +341,7 @@ class Arguments(object): Parses given arguments for the list-exchanges command. """ parser.add_argument( - '-1', + '-1', '--one-column', help='Print exchanges in one column', action='store_true', dest='print_one_column', @@ -376,7 +376,7 @@ class Arguments(object): self.hyperopt_options(hyperopt_cmd) # Add list-exchanges subcommand - list_exchanges_cmd = subparsers.add_parser('list-exchanges', help='List known exchanges.') + list_exchanges_cmd = subparsers.add_parser('list-exchanges', help='Print available exchanges.') list_exchanges_cmd.set_defaults(func=start_list_exchanges) self.list_exchanges_options(list_exchanges_cmd) From 09cd7db9b17f251212d9db7dc53660089b08b933 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 22:04:29 +0300 Subject: [PATCH 31/92] make flake happy --- freqtrade/arguments.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index a8e8af2ad..4f512e40e 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -376,7 +376,10 @@ class Arguments(object): self.hyperopt_options(hyperopt_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.' + ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) self.list_exchanges_options(list_exchanges_cmd) From ad9dc349e46fc0cc6438939f944cedc12aef07cc Mon Sep 17 00:00:00 2001 From: Misagh Date: Sat, 15 Jun 2019 12:20:32 +0200 Subject: [PATCH 32/92] edge cli should override stake_amount --- freqtrade/optimize/edge_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 8232c79c9..231493e4d 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -6,6 +6,7 @@ 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 freqtrade.arguments import Arguments @@ -32,6 +33,7 @@ class EdgeCli(object): self.config['exchange']['secret'] = '' self.config['exchange']['password'] = '' self.config['exchange']['uid'] = '' + self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.config['dry_run'] = True self.exchange = Exchange(self.config) self.strategy = StrategyResolver(self.config).strategy From 707118a63684290287fbfc53c6dcfd60e7e7f5e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:04:15 +0200 Subject: [PATCH 33/92] Test stake changed to unlimited --- freqtrade/tests/optimize/test_edge_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 5d16b0f2d..49d3cdd55 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -117,8 +117,10 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: def test_edge_init(mocker, edge_conf) -> None: patch_exchange(mocker) + edge_conf['stake_amount'] = 20 edge_cli = EdgeCli(edge_conf) assert edge_cli.config == edge_conf + assert edge_cli.config['stake_amount'] == 'unlimited' assert callable(edge_cli.edge.calculate) From a77d75eb431c29123b537b7dc1056bca1809560c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:14:07 +0200 Subject: [PATCH 34/92] Check log output since that's whats shown to users --- freqtrade/tests/test_configuration.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 82167125c..38f17fbea 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -15,7 +15,7 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration, set_loggers from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has +from freqtrade.tests.conftest import log_has, log_has_re @pytest.fixture(scope="function") @@ -473,22 +473,40 @@ def test_check_exchange(default_conf, caplog) -> None: # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'BITTREX'}) assert configuration.check_exchange(default_conf) + assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", + caplog.record_tuples) + caplog.clear() # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'binance'}) assert configuration.check_exchange(default_conf) + assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", + caplog.record_tuples) + caplog.clear() # Test an available exchange, supported by ccxt default_conf.get('exchange').update({'name': 'kraken'}) assert configuration.check_exchange(default_conf) + assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " + r"by the Freqtrade development team\. .*", + caplog.record_tuples) + caplog.clear() # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) assert not configuration.check_exchange(default_conf) + assert log_has_re(r"Exchange .* is known to not work with the bot yet\. " + r"Use it only for development and testing purposes\.", + caplog.record_tuples) + caplog.clear() # Test a 'bad' exchange with check_for_bad=False default_conf.get('exchange').update({'name': 'bitmex'}) assert configuration.check_exchange(default_conf, False) + assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " + r"by the Freqtrade development team\. .*", + caplog.record_tuples) + caplog.clear() # Test an invalid exchange default_conf.get('exchange').update({'name': 'unknown_exchange'}) From 36dd061be759167f1b376fbf94ec6bd93e77cfd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:19:18 +0200 Subject: [PATCH 35/92] Update slack link since the old one expired --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/developer.md | 2 +- docs/index.md | 2 +- docs/strategy-customization.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c511f44d..e15059f56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Few pointers for contributions: - Create your PR against the `develop` branch, not `master`. - New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100). -If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) +If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index 98dad1d2e..240b4f917 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel. -- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). +- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) @@ -172,7 +172,7 @@ to understand the requirements before sending your pull-requests. Coding is not a neccessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `master`. diff --git a/docs/developer.md b/docs/developer.md index 6ecb7f156..7f3dc76f6 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) where you can ask questions. ## Documentation diff --git a/docs/index.md b/docs/index.md index 9fbc0519c..63d6be75e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,7 +64,7 @@ To run this bot we recommend you a cloud instance with a minimum of: Help / Slack For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. -Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel. +Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) to join Slack channel. ## Ready to try? diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index fd9760bda..57c646aed 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -410,7 +410,7 @@ To get additional Ideas for strategies, head over to our [strategy repository](h Feel free to use any of them as inspiration for your own strategies. We're happy to accept Pull Requests containing new Strategies to that repo. -We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) which is a great place to get and/or share ideas. +We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) which is a great place to get and/or share ideas. ## Next step From e6cab6d7106d00bce437fa728475e8603fbfb81d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:13:12 +0200 Subject: [PATCH 36/92] Move get_args from multiple locations to conftest --- freqtrade/tests/conftest.py | 6 ++++++ freqtrade/tests/optimize/test_backtesting.py | 9 ++------- freqtrade/tests/optimize/test_edge_cli.py | 10 ++-------- freqtrade/tests/optimize/test_hyperopt.py | 3 +-- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index dcc69fcb1..5693d4d80 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -4,6 +4,7 @@ import logging import re from datetime import datetime from functools import reduce +from typing import List from unittest.mock import MagicMock, PropertyMock import arrow @@ -11,6 +12,7 @@ import pytest from telegram import Chat, Message, Update from freqtrade import constants, persistence +from freqtrade.arguments import Arguments from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange @@ -35,6 +37,10 @@ def log_has_re(line, logs): False) +def get_args(args) -> List[str]: + return Arguments(args, '').get_parsed_arg() + + def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 3f88a8d6c..cf32934c7 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -3,7 +3,6 @@ import json import math import random -from typing import List from unittest.mock import MagicMock import numpy as np @@ -12,7 +11,7 @@ import pytest from arrow import Arrow from freqtrade import DependencyException, constants -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe @@ -23,11 +22,7 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange - - -def get_args(args) -> List[str]: - return Arguments(args, '').get_parsed_arg() +from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange def trim_dictlist(dict_list, num): diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 5d16b0f2d..b46f353d8 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -2,19 +2,13 @@ # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments import json -from typing import List from unittest.mock import MagicMock -from freqtrade.arguments import Arguments from freqtrade.edge import PairInfo -from freqtrade.optimize import start_edge, setup_configuration +from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange - - -def get_args(args) -> List[str]: - return Arguments(args, '').get_parsed_arg() +from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a51d74dbb..baa5da545 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -16,8 +16,7 @@ from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange -from freqtrade.tests.optimize.test_backtesting import get_args +from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange @pytest.fixture(scope='function') From 442339cd279118dba4292a78ef2ca4fe2d25f144 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:13:24 +0200 Subject: [PATCH 37/92] Add tests for utils.py --- freqtrade/tests/test_utils.py | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 freqtrade/tests/test_utils.py diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py new file mode 100644 index 000000000..3526026e6 --- /dev/null +++ b/freqtrade/tests/test_utils.py @@ -0,0 +1,41 @@ +from freqtrade.utils import setup_configuration, start_list_exchanges +from freqtrade.tests.conftest import get_args, log_has, log_has_re +from freqtrade.state import RunMode + +import re + + +def test_setup_configuration(): + args = [ + '--config', 'config.json.example', + ] + + config = setup_configuration(get_args(args), RunMode.OTHER) + assert "exchange" in config + assert config['exchange']['key'] == '' + assert config['exchange']['secret'] == '' + + +def test_list_exchanges(capsys): + + args = [ + "list-exchanges", + ] + + start_list_exchanges(get_args(args)) + captured = capsys.readouterr() + assert re.match(r"Exchanges supported by ccxt and available.*", captured.out) + assert re.match(r".*binance,.*", captured.out) + assert re.match(r".*bittrex,.*", captured.out) + + # Test with --one-column + args = [ + "list-exchanges", + "--one-column", + ] + + start_list_exchanges(get_args(args)) + captured = capsys.readouterr() + assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out) + assert re.search(r"^binance$", captured.out, re.MULTILINE) + assert re.search(r"^bittrex$", captured.out, re.MULTILINE) From 114de8a0259914804750fc5b341ac0cc45832e34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:13:56 +0200 Subject: [PATCH 38/92] Remove unused imports --- freqtrade/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index 3526026e6..7550efb23 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -1,5 +1,5 @@ from freqtrade.utils import setup_configuration, start_list_exchanges -from freqtrade.tests.conftest import get_args, log_has, log_has_re +from freqtrade.tests.conftest import get_args from freqtrade.state import RunMode import re From 9035e0b695e710b4bab719e35b57e5620c450cf7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:39:43 +0200 Subject: [PATCH 39/92] Update function due to merge of #1926 --- freqtrade/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 45bd9fd3f..324b54a4e 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -3,7 +3,7 @@ from argparse import Namespace from typing import Any, Dict from freqtrade.configuration import Configuration -from freqtrade.exchange import supported_exchanges +from freqtrade.exchange import available_exchanges from freqtrade.state import RunMode @@ -34,7 +34,7 @@ def start_list_exchanges(args: Namespace) -> None: """ if args.print_one_column: - print('\n'.join(supported_exchanges())) + print('\n'.join(available_exchanges())) else: print(f"Exchanges supported by ccxt and available for Freqtrade: " - f"{', '.join(supported_exchanges())}") + f"{', '.join(available_exchanges())}") From 583d70ec9c535d6b2ad6275818096bcb2b9fac49 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 14:18:30 +0200 Subject: [PATCH 40/92] add plot module proto --- freqtrade/plot/__init__.py | 0 freqtrade/plot/plotting.py | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 freqtrade/plot/__init__.py create mode 100644 freqtrade/plot/plotting.py diff --git a/freqtrade/plot/__init__.py b/freqtrade/plot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py new file mode 100644 index 000000000..e04b51726 --- /dev/null +++ b/freqtrade/plot/plotting.py @@ -0,0 +1,13 @@ +import logging + + +logger = logging.getLogger(__name__) + + +try: + from plotly import tools + from plotly.offline import plot + import plotly.graph_objs as go +except ImportError: + logger.exception("Module plotly not found \n Please install using `pip install plotly`") + exit() From 68af6d415157183a33472eb8dfd2070d3db2e3f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 07:00:57 +0200 Subject: [PATCH 41/92] Move plot-functions to plotting module --- freqtrade/plot/plotting.py | 177 +++++++++++++++++++++++++++++++++++++ scripts/plot_dataframe.py | 170 ++--------------------------------- 2 files changed, 185 insertions(+), 162 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e04b51726..7e815bdd7 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,5 +1,7 @@ import logging +from typing import List +import pandas as pd logger = logging.getLogger(__name__) @@ -11,3 +13,178 @@ try: except ImportError: logger.exception("Module plotly not found \n Please install using `pip install plotly`") exit() + + +def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: + """ + Generator all the indicator selected by the user for a specific row + :param fig: Plot figure to append to + :param row: row number for this plot + :param indicators: List of indicators present in the dataframe + :param data: candlestick DataFrame + """ + for indicator in indicators: + if indicator in data: + # TODO: Replace all Scatter with Scattergl for performance!! + scattergl = go.Scatter( + x=data['date'], + y=data[indicator], + mode='lines', + name=indicator + ) + fig.append_trace(scattergl, row, 1) + else: + logger.info( + 'Indicator "%s" ignored. Reason: This indicator is not found ' + 'in your strategy.', + indicator + ) + + return fig + + +def plot_trades(fig, trades: pd.DataFrame): + """ + Plot trades to "fig" + """ + # Trades can be empty + if trades is not None: + trade_buys = go.Scatter( + x=trades["open_time"], + y=trades["open_rate"], + mode='markers', + name='trade_buy', + marker=dict( + symbol='square-open', + size=11, + line=dict(width=2), + color='green' + ) + ) + trade_sells = go.Scatter( + x=trades["close_time"], + y=trades["close_rate"], + mode='markers', + name='trade_sell', + marker=dict( + symbol='square-open', + size=11, + line=dict(width=2), + color='red' + ) + ) + fig.append_trace(trade_buys, 1, 1) + fig.append_trace(trade_sells, 1, 1) + return fig + + +def generate_graph( + pair: str, + data: pd.DataFrame, + trades: pd.DataFrame = None, + indicators1: List[str] = [], + indicators2: List[str] = [], +) -> tools.make_subplots: + """ + Generate the graph from the data generated by Backtesting or from DB + Volume will always be ploted in row2, so Row 1 and are to our disposal for custom indicators + :param pair: Pair to Display on the graph + :param data: OHLCV DataFrame containing indicators and buy/sell signals + :param trades: All trades created + :param indicators1: List containing Main plot indicators + :param indicators2: List containing Sub plot indicators + :return: None + """ + + # Define the graph + fig = tools.make_subplots( + rows=3, + cols=1, + shared_xaxes=True, + row_width=[1, 1, 4], + vertical_spacing=0.0001, + ) + fig['layout'].update(title=pair) + fig['layout']['yaxis1'].update(title='Price') + fig['layout']['yaxis2'].update(title='Volume') + fig['layout']['yaxis3'].update(title='Other') + fig['layout']['xaxis']['rangeslider'].update(visible=False) + + # Common information + candles = go.Candlestick( + x=data.date, + open=data.open, + high=data.high, + low=data.low, + close=data.close, + name='Price' + ) + fig.append_trace(candles, 1, 1) + + if 'buy' in data.columns: + df_buy = data[data['buy'] == 1] + buys = go.Scatter( + x=df_buy.date, + y=df_buy.close, + mode='markers', + name='buy', + marker=dict( + symbol='triangle-up-dot', + size=9, + line=dict(width=1), + color='green', + ) + ) + fig.append_trace(buys, 1, 1) + + if 'sell' in data.columns: + df_sell = data[data['sell'] == 1] + sells = go.Scatter( + x=df_sell.date, + y=df_sell.close, + mode='markers', + name='sell', + marker=dict( + symbol='triangle-down-dot', + size=9, + line=dict(width=1), + color='red', + ) + ) + fig.append_trace(sells, 1, 1) + + if 'bb_lowerband' in data and 'bb_upperband' in data: + bb_lower = go.Scatter( + x=data.date, + y=data.bb_lowerband, + name='BB lower', + line={'color': 'rgba(255,255,255,0)'}, + ) + bb_upper = go.Scatter( + x=data.date, + y=data.bb_upperband, + name='BB upper', + fill="tonexty", + fillcolor="rgba(0,176,246,0.2)", + line={'color': 'rgba(255,255,255,0)'}, + ) + fig.append_trace(bb_lower, 1, 1) + fig.append_trace(bb_upper, 1, 1) + + # Add indicators to main plot + fig = generate_row(fig=fig, row=1, indicators=indicators1, data=data) + + fig = plot_trades(fig, trades) + + # Volume goes to row 2 + volume = go.Bar( + x=data['date'], + y=data['volume'], + name='Volume' + ) + fig.append_trace(volume, 2, 1) + + # Add indicators to seperate row + fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data) + + return fig diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 4f8ffb32b..897b0c917 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -31,15 +31,14 @@ from pathlib import Path from typing import Any, Dict, List import pandas as pd -import plotly.graph_objs as go import pytz -from plotly import tools from plotly.offline import plot from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data +from freqtrade.plot.plotting import generate_graph from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration from freqtrade.persistence import Trade @@ -96,7 +95,10 @@ def generate_plot_file(fig, pair, ticker_interval, is_last) -> None: Path("user_data/plots").mkdir(parents=True, exist_ok=True) - plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), auto_open=False) + plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), + auto_open=False, + include_plotlyjs='https://cdn.plot.ly/plotly-1.47.4.min.js' + ) if is_last: plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False) @@ -186,162 +188,6 @@ def extract_trades_of_period(dataframe, trades) -> pd.DataFrame: return trades -def generate_graph( - pair: str, - trades: pd.DataFrame, - data: pd.DataFrame, - indicators1: str, - indicators2: str - ) -> tools.make_subplots: - """ - Generate the graph from the data generated by Backtesting or from DB - :param pair: Pair to Display on the graph - :param trades: All trades created - :param data: Dataframe - :indicators1: String Main plot indicators - :indicators2: String Sub plot indicators - :return: None - """ - - # Define the graph - fig = tools.make_subplots( - rows=3, - cols=1, - shared_xaxes=True, - row_width=[1, 1, 4], - vertical_spacing=0.0001, - ) - fig['layout'].update(title=pair) - fig['layout']['yaxis1'].update(title='Price') - fig['layout']['yaxis2'].update(title='Volume') - fig['layout']['yaxis3'].update(title='Other') - fig['layout']['xaxis']['rangeslider'].update(visible=False) - - # Common information - candles = go.Candlestick( - x=data.date, - open=data.open, - high=data.high, - low=data.low, - close=data.close, - name='Price' - ) - - df_buy = data[data['buy'] == 1] - buys = go.Scattergl( - x=df_buy.date, - y=df_buy.close, - mode='markers', - name='buy', - marker=dict( - symbol='triangle-up-dot', - size=9, - line=dict(width=1), - color='green', - ) - ) - df_sell = data[data['sell'] == 1] - sells = go.Scattergl( - x=df_sell.date, - y=df_sell.close, - mode='markers', - name='sell', - marker=dict( - symbol='triangle-down-dot', - size=9, - line=dict(width=1), - color='red', - ) - ) - - trade_buys = go.Scattergl( - x=trades["open_time"], - y=trades["open_rate"], - mode='markers', - name='trade_buy', - marker=dict( - symbol='square-open', - size=11, - line=dict(width=2), - color='green' - ) - ) - trade_sells = go.Scattergl( - x=trades["close_time"], - y=trades["close_rate"], - mode='markers', - name='trade_sell', - marker=dict( - symbol='square-open', - size=11, - line=dict(width=2), - color='red' - ) - ) - - # Row 1 - fig.append_trace(candles, 1, 1) - - if 'bb_lowerband' in data and 'bb_upperband' in data: - bb_lower = go.Scatter( - x=data.date, - y=data.bb_lowerband, - name='BB lower', - line={'color': 'rgba(255,255,255,0)'}, - ) - bb_upper = go.Scatter( - x=data.date, - y=data.bb_upperband, - name='BB upper', - fill="tonexty", - fillcolor="rgba(0,176,246,0.2)", - line={'color': 'rgba(255,255,255,0)'}, - ) - fig.append_trace(bb_lower, 1, 1) - fig.append_trace(bb_upper, 1, 1) - - fig = generate_row(fig=fig, row=1, raw_indicators=indicators1, data=data) - fig.append_trace(buys, 1, 1) - fig.append_trace(sells, 1, 1) - fig.append_trace(trade_buys, 1, 1) - fig.append_trace(trade_sells, 1, 1) - - # Row 2 - volume = go.Bar( - x=data['date'], - y=data['volume'], - name='Volume' - ) - fig.append_trace(volume, 2, 1) - - # Row 3 - fig = generate_row(fig=fig, row=3, raw_indicators=indicators2, data=data) - - return fig - - -def generate_row(fig, row, raw_indicators, data) -> tools.make_subplots: - """ - Generator all the indicator selected by the user for a specific row - """ - for indicator in raw_indicators.split(','): - if indicator in data: - scattergl = go.Scattergl( - x=data['date'], - y=data[indicator], - name=indicator - ) - fig.append_trace(scattergl, row, 1) - else: - logger.info( - 'Indicator "%s" ignored. Reason: This indicator is not found ' - 'in your strategy.', - indicator - ) - - return fig - - def plot_parse_args(args: List[str]) -> Namespace: """ Parse args passed to the script @@ -411,10 +257,10 @@ def analyse_and_plot_pairs(args: Namespace): fig = generate_graph( pair=pair, - trades=trades, data=dataframe, - indicators1=args.indicators1, - indicators2=args.indicators2 + trades=trades, + indicators1=args.indicators1.split(","), + indicators2=args.indicators2.split(",") ) is_last = (False, True)[pair_counter == len(tickers)] From 6df0b39f8151c31b89d4acbf06a9654fa6c56976 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 20:02:17 +0200 Subject: [PATCH 42/92] Cleanup plot_dataframe a bit --- scripts/plot_dataframe.py | 99 +++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 897b0c917..1d3b24449 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -51,7 +51,7 @@ _CONF: Dict[str, Any] = {} timeZone = pytz.UTC -def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: +def load_trades(args: Namespace, pair: str) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: persistence.init(args.db_url, clean_open_orders=False) @@ -96,9 +96,7 @@ def generate_plot_file(fig, pair, ticker_interval, is_last) -> None: Path("user_data/plots").mkdir(parents=True, exist_ok=True) plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), - auto_open=False, - include_plotlyjs='https://cdn.plot.ly/plotly-1.47.4.min.js' - ) + auto_open=False) if is_last: plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False) @@ -133,14 +131,13 @@ def get_trading_env(args: Namespace): return [strategy, exchange, pairs] -def get_tickers_data(strategy, exchange, pairs: List[str], args): +def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, live: bool): """ Get tickers data for each pairs on live or local, option defined in args - :return: dictinnary of tickers. output format: {'pair': tickersdata} + :return: dictionary of tickers. output format: {'pair': tickersdata} """ ticker_interval = strategy.ticker_interval - timerange = Arguments.parse_timerange(args.timerange) tickers = history.load_data( datadir=Path(str(_CONF.get("datadir"))), @@ -184,10 +181,53 @@ def extract_trades_of_period(dataframe, trades) -> pd.DataFrame: Compare trades and backtested pair DataFrames to get trades performed on backtested period :return: the DataFrame of a trades of period """ - trades = trades.loc[trades['open_time'] >= dataframe.iloc[0]['date']] + trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & + (trades['close_time'] <= dataframe.iloc[-1]['date'])] return trades +def analyse_and_plot_pairs(args: Namespace): + """ + From arguments provided in cli: + -Initialise backtest env + -Get tickers data + -Generate Dafaframes populated with indicators and signals + -Load trades excecuted on same periods + -Generate Plotly plot objects + -Generate plot files + :return: None + """ + strategy, exchange, pairs = get_trading_env(args) + # Set timerange to use + timerange = Arguments.parse_timerange(args.timerange) + ticker_interval = strategy.ticker_interval + + tickers = get_tickers_data(strategy, exchange, pairs, timerange, args.live) + pair_counter = 0 + for pair, data in tickers.items(): + pair_counter += 1 + logger.info("analyse pair %s", pair) + tickers = {} + tickers[pair] = data + dataframe = generate_dataframe(strategy, tickers, pair) + + trades = load_trades(args, pair) + trades = extract_trades_of_period(dataframe, trades) + + fig = generate_graph( + pair=pair, + data=dataframe, + trades=trades, + indicators1=args.indicators1.split(","), + indicators2=args.indicators2.split(",") + ) + + is_last = (False, True)[pair_counter == len(tickers)] + generate_plot_file(fig, pair, ticker_interval, is_last) + + logger.info('End of ploting process %s plots generated', pair_counter) + + def plot_parse_args(args: List[str]) -> Namespace: """ Parse args passed to the script @@ -226,49 +266,6 @@ def plot_parse_args(args: List[str]) -> Namespace: arguments.backtesting_options(arguments.parser) return arguments.parse_args() - -def analyse_and_plot_pairs(args: Namespace): - """ - From arguments provided in cli: - -Initialise backtest env - -Get tickers data - -Generate Dafaframes populated with indicators and signals - -Load trades excecuted on same periods - -Generate Plotly plot objects - -Generate plot files - :return: None - """ - strategy, exchange, pairs = get_trading_env(args) - # Set timerange to use - timerange = Arguments.parse_timerange(args.timerange) - ticker_interval = strategy.ticker_interval - - tickers = get_tickers_data(strategy, exchange, pairs, args) - pair_counter = 0 - for pair, data in tickers.items(): - pair_counter += 1 - logger.info("analyse pair %s", pair) - tickers = {} - tickers[pair] = data - dataframe = generate_dataframe(strategy, tickers, pair) - - trades = load_trades(args, pair, timerange) - trades = extract_trades_of_period(dataframe, trades) - - fig = generate_graph( - pair=pair, - data=dataframe, - trades=trades, - indicators1=args.indicators1.split(","), - indicators2=args.indicators2.split(",") - ) - - is_last = (False, True)[pair_counter == len(tickers)] - generate_plot_file(fig, pair, ticker_interval, is_last) - - logger.info('End of ploting process %s plots generated', pair_counter) - - def main(sysargv: List[str]) -> None: """ This function will initiate the bot and start the trading loop. From e0a1e5417fbb8e36bc947e29398f05754b565959 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 20:23:16 +0200 Subject: [PATCH 43/92] sanity checks before plotting, cleanup --- freqtrade/plot/plotting.py | 63 ++++++++++++++++++++------------------ scripts/plot_dataframe.py | 29 +++++++++++------- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 7e815bdd7..a18b7bf70 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -25,8 +25,7 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m """ for indicator in indicators: if indicator in data: - # TODO: Replace all Scatter with Scattergl for performance!! - scattergl = go.Scatter( + scattergl = go.Scattergl( x=data['date'], y=data[indicator], mode='lines', @@ -48,7 +47,7 @@ def plot_trades(fig, trades: pd.DataFrame): Plot trades to "fig" """ # Trades can be empty - if trades is not None: + if trades is not None and len(trades) > 0: trade_buys = go.Scatter( x=trades["open_time"], y=trades["open_rate"], @@ -123,44 +122,50 @@ def generate_graph( if 'buy' in data.columns: df_buy = data[data['buy'] == 1] - buys = go.Scatter( - x=df_buy.date, - y=df_buy.close, - mode='markers', - name='buy', - marker=dict( - symbol='triangle-up-dot', - size=9, - line=dict(width=1), - color='green', + if len(df_buy) > 0: + buys = go.Scattergl( + x=df_buy.date, + y=df_buy.close, + mode='markers', + name='buy', + marker=dict( + symbol='triangle-up-dot', + size=9, + line=dict(width=1), + color='green', + ) ) - ) - fig.append_trace(buys, 1, 1) + fig.append_trace(buys, 1, 1) + else: + logger.warning("No buy-signals found.") if 'sell' in data.columns: df_sell = data[data['sell'] == 1] - sells = go.Scatter( - x=df_sell.date, - y=df_sell.close, - mode='markers', - name='sell', - marker=dict( - symbol='triangle-down-dot', - size=9, - line=dict(width=1), - color='red', + if len(df_sell) > 0: + sells = go.Scattergl( + x=df_sell.date, + y=df_sell.close, + mode='markers', + name='sell', + marker=dict( + symbol='triangle-down-dot', + size=9, + line=dict(width=1), + color='red', + ) ) - ) - fig.append_trace(sells, 1, 1) + fig.append_trace(sells, 1, 1) + else: + logger.warning("No sell-signals found.") if 'bb_lowerband' in data and 'bb_upperband' in data: - bb_lower = go.Scatter( + bb_lower = go.Scattergl( x=data.date, y=data.bb_lowerband, name='BB lower', line={'color': 'rgba(255,255,255,0)'}, ) - bb_upper = go.Scatter( + bb_upper = go.Scattergl( x=data.date, y=data.bb_upperband, name='BB upper', diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 1d3b24449..84e18e5cd 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -51,11 +51,18 @@ _CONF: Dict[str, Any] = {} timeZone = pytz.UTC -def load_trades(args: Namespace, pair: str) -> pd.DataFrame: - trades: pd.DataFrame = pd.DataFrame() - if args.db_url: - persistence.init(args.db_url, clean_open_orders=False) +def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: + """ + Load trades, either from a DB (using dburl) or via a backtest export file. + :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) + :param exportfilename: Path to a file exported from backtesting + :returns: Dataframe containing Trades + """ + # TODO: Document and move to btanalysis + trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) + if db_url: + persistence.init(db_url, clean_open_orders=False) columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] @@ -68,18 +75,15 @@ def load_trades(args: Namespace, pair: str) -> pd.DataFrame: t.open_rate, t.close_rate, t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None) - for t in Trade.query.filter(Trade.pair.is_(pair)).all()], + for t in Trade.query.all()], columns=columns) - elif args.exportfilename: + elif exportfilename: - file = Path(args.exportfilename) + file = Path(exportfilename) if file.exists(): trades = load_backtest_data(file) - else: - trades = pd.DataFrame([], columns=BT_DATA_COLUMNS) - return trades @@ -181,6 +185,7 @@ def extract_trades_of_period(dataframe, trades) -> pd.DataFrame: Compare trades and backtested pair DataFrames to get trades performed on backtested period :return: the DataFrame of a trades of period """ + # TODO: Document and move to btanalysis (?) trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & (trades['close_time'] <= dataframe.iloc[-1]['date'])] return trades @@ -211,7 +216,9 @@ def analyse_and_plot_pairs(args: Namespace): tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - trades = load_trades(args, pair) + trades = load_trades(pair, db_url=args.db_url, + exportfilename=args.exportfilename) + trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) fig = generate_graph( From b1a01345f9db4bf45b128e31464498fd14af003e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 07:19:21 +0200 Subject: [PATCH 44/92] Add better hover tip --- freqtrade/plot/plotting.py | 19 +++++++++++++------ scripts/plot_dataframe.py | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a18b7bf70..a8bd032ab 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,7 @@ import logging - from typing import List + +import arrow import pandas as pd logger = logging.getLogger(__name__) @@ -12,7 +13,7 @@ try: import plotly.graph_objs as go except ImportError: logger.exception("Module plotly not found \n Please install using `pip install plotly`") - exit() + exit(1) def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: @@ -25,9 +26,10 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m """ for indicator in indicators: if indicator in data: - scattergl = go.Scattergl( + # TODO: Figure out why scattergl causes problems + scattergl = go.Scatter( x=data['date'], - y=data[indicator], + y=data[indicator].values, mode='lines', name=indicator ) @@ -60,9 +62,14 @@ def plot_trades(fig, trades: pd.DataFrame): color='green' ) ) + # Create description for sell summarizing the trade + desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, " + f"{row['duration']}min", + axis=1) trade_sells = go.Scatter( x=trades["close_time"], y=trades["close_rate"], + text=desc, mode='markers', name='trade_sell', marker=dict( @@ -123,7 +130,7 @@ def generate_graph( if 'buy' in data.columns: df_buy = data[data['buy'] == 1] if len(df_buy) > 0: - buys = go.Scattergl( + buys = go.Scatter( x=df_buy.date, y=df_buy.close, mode='markers', @@ -142,7 +149,7 @@ def generate_graph( if 'sell' in data.columns: df_sell = data[data['sell'] == 1] if len(df_sell) > 0: - sells = go.Scattergl( + sells = go.Scatter( x=df_sell.date, y=df_sell.close, mode='markers', diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 84e18e5cd..7457aadc4 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -216,7 +216,7 @@ def analyse_and_plot_pairs(args: Namespace): tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - trades = load_trades(pair, db_url=args.db_url, + trades = load_trades(db_url=args.db_url, exportfilename=args.exportfilename) trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) From 6347161975591e6cb0d9721df52de98003c13ef2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 May 2019 20:26:46 +0200 Subject: [PATCH 45/92] don't use print in plot_dataframe --- scripts/plot_dataframe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7457aadc4..a20d8abae 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -67,7 +67,7 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: "open_rate", "close_rate", "duration"] for x in Trade.query.all(): - print("date: {}".format(x.open_date)) + logger.info("date: {}".format(x.open_date)) trades = pd.DataFrame([(t.pair, t.calc_profit(), t.open_date.replace(tzinfo=timeZone), @@ -114,7 +114,6 @@ def get_trading_env(args: Namespace): # Load the configuration _CONF.update(setup_configuration(args, RunMode.BACKTEST)) - print(_CONF) pairs = args.pairs.split(',') if pairs is None: @@ -161,7 +160,7 @@ def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, if data.empty: del tickers[pair] logger.info( - 'An issue occured while retreiving datas of %s pair, please retry ' + 'An issue occured while retreiving data of %s pair, please retry ' 'using -l option for live or --refresh-pairs-cached', pair) return tickers @@ -273,6 +272,7 @@ def plot_parse_args(args: List[str]) -> Namespace: arguments.backtesting_options(arguments.parser) return arguments.parse_args() + def main(sysargv: List[str]) -> None: """ This function will initiate the bot and start the trading loop. From cae218546087552ba890cb39425e6f6820058ad7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 May 2019 06:41:55 +0200 Subject: [PATCH 46/92] Move generate_plot to plotting.py --- freqtrade/plot/plotting.py | 20 ++++++++++++++++++++ scripts/plot_dataframe.py | 22 ++-------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a8bd032ab..037516a27 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -3,6 +3,7 @@ from typing import List import arrow import pandas as pd +from pathlib import Path logger = logging.getLogger(__name__) @@ -200,3 +201,22 @@ def generate_graph( fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data) return fig + + +def generate_plot_file(fig, pair, ticker_interval) -> None: + """ + 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 + :return: None + """ + logger.info('Generate plot file for %s', pair) + + pair_name = pair.replace("/", "_") + file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' + + Path("user_data/plots").mkdir(parents=True, exist_ok=True) + + plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), + auto_open=False) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index a20d8abae..bccf98261 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -38,7 +38,7 @@ from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data -from freqtrade.plot.plotting import generate_graph +from freqtrade.plot.plotting import generate_graph, generate_plot_file from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration from freqtrade.persistence import Trade @@ -87,23 +87,6 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: return trades -def generate_plot_file(fig, pair, ticker_interval, is_last) -> None: - """ - Generate a plot html file from pre populated fig plotly object - :return: None - """ - logger.info('Generate plot file for %s', pair) - - pair_name = pair.replace("/", "_") - file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' - - Path("user_data/plots").mkdir(parents=True, exist_ok=True) - - plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), - auto_open=False) - if is_last: - plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False) - def get_trading_env(args: Namespace): """ @@ -228,8 +211,7 @@ def analyse_and_plot_pairs(args: Namespace): indicators2=args.indicators2.split(",") ) - is_last = (False, True)[pair_counter == len(tickers)] - generate_plot_file(fig, pair, ticker_interval, is_last) + generate_plot_file(fig, pair, ticker_interval) logger.info('End of ploting process %s plots generated', pair_counter) From 2891d7cccbc61835cc28d40f62fb9cce3bda8720 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jun 2019 20:17:23 +0200 Subject: [PATCH 47/92] Add initial plotting test --- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/test_plotting.py | 52 ++++++++++++++++++++++++++++++++ scripts/plot_dataframe.py | 4 +-- 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 freqtrade/tests/test_plotting.py diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 037516a27..b1e32c4fb 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -91,7 +91,7 @@ def generate_graph( trades: pd.DataFrame = None, indicators1: List[str] = [], indicators2: List[str] = [], -) -> tools.make_subplots: +) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB Volume will always be ploted in row2, so Row 1 and are to our disposal for custom indicators diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py new file mode 100644 index 000000000..1f4873f4a --- /dev/null +++ b/freqtrade/tests/test_plotting.py @@ -0,0 +1,52 @@ + +from unittest.mock import MagicMock + +import plotly.graph_objs as go + +from freqtrade.arguments import Arguments, TimeRange +from freqtrade.data import history +from freqtrade.plot.plotting import (generate_graph, generate_plot_file, + generate_row, plot_trades) + + +def fig_generating_mock(fig, *args, **kwargs): + """ Return Fig - used to mock generate_row and plot_trades""" + return fig + + +def test_generate_row(): + # TODO: implement me + pass + + +def test_plot_trades(): + # TODO: implement me + pass + + +def test_generate_graph(default_conf, mocker): + row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', + MagicMock(side_effect=fig_generating_mock)) + trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', + MagicMock(side_effect=fig_generating_mock)) + + timerange = TimeRange(None, 'line', 0, -100) + data = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='1m', + datadir=None, timerange=timerange) + + indicators1 = [] + indicators2 = [] + fig = generate_graph(pair="UNITTEST/BTC", data=data, trades=None, + indicators1=indicators1, indicators2=indicators2) + assert isinstance(fig, go.Figure) + assert fig.layout.title.text == "UNITTEST/BTC" + figure = fig.layout.figure + # Candlesticks are plotted first + assert isinstance(figure.data[0], go.Candlestick) + assert figure.data[0].name == "Price" + + assert isinstance(figure.data[1], go.Bar) + assert figure.data[1].name == "Volume" + + assert row_mock.call_count == 2 + assert trades_mock.call_count == 1 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index bccf98261..8bed81985 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -32,7 +32,6 @@ from typing import Any, Dict, List import pandas as pd import pytz -from plotly.offline import plot from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange @@ -87,7 +86,6 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: return trades - def get_trading_env(args: Namespace): """ Initalize freqtrade Exchange and Strategy, split pairs recieved in parameter @@ -132,7 +130,7 @@ def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, refresh_pairs=_CONF.get('refresh_pairs', False), timerange=timerange, exchange=Exchange(_CONF), - live=args.live, + live=live, ) # No ticker found, impossible to download, len mismatch From 6db4e05aef7a97531041b20417448c0551974753 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jun 2019 06:45:36 +0200 Subject: [PATCH 48/92] Improve plotting tests --- freqtrade/tests/test_plotting.py | 83 +++++++++++++++++++++++++++----- requirements-dev.txt | 1 + 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 1f4873f4a..d9fb3b338 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -7,13 +7,19 @@ from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.plot.plotting import (generate_graph, generate_plot_file, generate_row, plot_trades) - +from freqtrade.strategy.default_strategy import DefaultStrategy +from freqtrade.tests.conftest import log_has, log_has_re def fig_generating_mock(fig, *args, **kwargs): """ Return Fig - used to mock generate_row and plot_trades""" return fig +def find_trace_in_fig_data(data, search_string: str): + matches = filter(lambda x: x.name == search_string, data) + return next(matches) + + def test_generate_row(): # TODO: implement me pass @@ -24,29 +30,84 @@ def test_plot_trades(): pass -def test_generate_graph(default_conf, mocker): +def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', MagicMock(side_effect=fig_generating_mock)) trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', MagicMock(side_effect=fig_generating_mock)) - timerange = TimeRange(None, 'line', 0, -100) - data = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='1m', + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + data = history.load_pair_history(pair=pair, ticker_interval='1m', datadir=None, timerange=timerange) + data['buy'] = 0 + data['sell'] = 0 indicators1 = [] indicators2 = [] - fig = generate_graph(pair="UNITTEST/BTC", data=data, trades=None, + fig = generate_graph(pair=pair, data=data, trades=None, indicators1=indicators1, indicators2=indicators2) assert isinstance(fig, go.Figure) - assert fig.layout.title.text == "UNITTEST/BTC" + assert fig.layout.title.text == pair figure = fig.layout.figure - # Candlesticks are plotted first - assert isinstance(figure.data[0], go.Candlestick) - assert figure.data[0].name == "Price" - assert isinstance(figure.data[1], go.Bar) - assert figure.data[1].name == "Volume" + assert len(figure.data) == 2 + # Candlesticks are plotted first + candles = find_trace_in_fig_data(figure.data, "Price") + assert isinstance(candles, go.Candlestick) + + volume = find_trace_in_fig_data(figure.data, "Volume") + assert isinstance(volume, go.Bar) + + assert row_mock.call_count == 2 + assert trades_mock.call_count == 1 + + assert log_has("No buy-signals found.", caplog.record_tuples) + assert log_has("No sell-signals found.", caplog.record_tuples) + + +def test_generate_graph_no_trades(default_conf, mocker): + row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', + MagicMock(side_effect=fig_generating_mock)) + trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', + 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', + datadir=None, timerange=timerange) + + # Generate buy/sell signals and indicators + strat = DefaultStrategy(default_conf) + data = strat.analyze_ticker(data, {'pair': pair}) + + indicators1 = [] + indicators2 = [] + fig = generate_graph(pair=pair, data=data, trades=None, + indicators1=indicators1, indicators2=indicators2) + assert isinstance(fig, go.Figure) + assert fig.layout.title.text == pair + figure = fig.layout.figure + + assert len(figure.data) == 6 + # Candlesticks are plotted first + candles = find_trace_in_fig_data(figure.data, "Price") + assert isinstance(candles, go.Candlestick) + + volume = find_trace_in_fig_data(figure.data, "Volume") + assert isinstance(volume, go.Bar) + + buy = find_trace_in_fig_data(figure.data, "buy") + assert isinstance(buy, go.Scatter) + # All buy-signals should be plotted + assert int(data.buy.sum()) == len(buy.x) + + sell = find_trace_in_fig_data(figure.data, "sell") + assert isinstance(sell, go.Scatter) + # All buy-signals should be plotted + assert int(data.sell.sum()) == len(sell.x) + + assert find_trace_in_fig_data(figure.data, "BB lower") + assert find_trace_in_fig_data(figure.data, "BB upper") assert row_mock.call_count == 2 assert trades_mock.call_count == 1 diff --git a/requirements-dev.txt b/requirements-dev.txt index 315033847..c8dd8b0b9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ # Include all requirements to run the bot. -r requirements.txt +-r requirements-plot.txt flake8==3.7.7 flake8-type-annotations==0.1.0 From 9f5ca82f485bece4452b46c5a1c4ea7d2315cdb6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:32:12 +0200 Subject: [PATCH 49/92] Add more tests --- freqtrade/tests/test_plotting.py | 54 +++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index d9fb3b338..e264ef6b3 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -1,7 +1,9 @@ from unittest.mock import MagicMock +from plotly import tools import plotly.graph_objs as go +from copy import deepcopy from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history @@ -10,6 +12,7 @@ from freqtrade.plot.plotting import (generate_graph, generate_plot_file, from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re + def fig_generating_mock(fig, *args, **kwargs): """ Return Fig - used to mock generate_row and plot_trades""" return fig @@ -20,14 +23,55 @@ def find_trace_in_fig_data(data, search_string: str): return next(matches) -def test_generate_row(): - # TODO: implement me - pass +def generage_empty_figure(): + return tools.make_subplots( + rows=3, + cols=1, + shared_xaxes=True, + row_width=[1, 1, 4], + vertical_spacing=0.0001, + ) + +def test_generate_row(default_conf, caplog): + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + + data = history.load_pair_history(pair=pair, ticker_interval='1m', + datadir=None, timerange=timerange) + indicators1 = ["ema10"] + indicators2 = ["macd"] + + # Generate buy/sell signals and indicators + strat = DefaultStrategy(default_conf) + data = strat.analyze_ticker(data, {'pair': pair}) + fig = generage_empty_figure() + + # Row 1 + fig1 = generate_row(fig=deepcopy(fig), row=1, indicators=indicators1, data=data) + figure = fig1.layout.figure + ema10 = find_trace_in_fig_data(figure.data, "ema10") + assert isinstance(ema10, go.Scatter) + assert ema10.yaxis == "y" + + fig2 = generate_row(fig=deepcopy(fig), row=3, indicators=indicators2, data=data) + figure = fig2.layout.figure + macd = find_trace_in_fig_data(figure.data, "macd") + assert isinstance(macd, go.Scatter) + assert macd.yaxis == "y3" + + # No indicator found + fig3 = generate_row(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) + assert fig == fig3 + assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples) def test_plot_trades(): - # TODO: implement me - pass + fig1 = generage_empty_figure() + # nothing happens when no trades are available + fig = plot_trades(fig1, None) + assert fig == fig1 + + # TODO: implement tests that do something def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): From c7643e142b7e6ba0c7e9cbc422409caa38608952 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:41:05 +0200 Subject: [PATCH 50/92] Move load_trades to bt_anlaysis --- freqtrade/data/btanalysis.py | 43 ++++++++++++++++++++++++++++++++++++ scripts/plot_dataframe.py | 43 +----------------------------------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6fce4361b..fb120e521 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -1,12 +1,18 @@ """ Helpers when analyzing backtest data """ +import logging from pathlib import Path import numpy as np import pandas as pd +import pytz +from freqtrade import persistence from freqtrade.misc import json_load +from freqtrade.persistence import Trade + +logger = logging.getLogger(__name__) # must align with columns in backtest.py BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration", @@ -65,3 +71,40 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int df2 = df2.set_index('date') df_final = df2.resample(freq)[['pair']].count() return df_final[df_final['pair'] > max_open_trades] + + +def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: + """ + Load trades, either from a DB (using dburl) or via a backtest export file. + :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) + :param exportfilename: Path to a file exported from backtesting + :returns: Dataframe containing Trades + """ + timeZone = pytz.UTC + + trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) + + if db_url: + persistence.init(db_url, clean_open_orders=False) + columns = ["pair", "profit", "open_time", "close_time", + "open_rate", "close_rate", "duration"] + + for x in Trade.query.all(): + logger.info("date: {}".format(x.open_date)) + + trades = pd.DataFrame([(t.pair, t.calc_profit(), + t.open_date.replace(tzinfo=timeZone), + t.close_date.replace(tzinfo=timeZone) if t.close_date else None, + t.open_rate, t.close_rate, + t.close_date.timestamp() - t.open_date.timestamp() + if t.close_date else None) + for t in Trade.query.all()], + columns=columns) + + elif exportfilename: + + file = Path(exportfilename) + if file.exists(): + trades = load_backtest_data(file) + + return trades diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 8bed81985..005148d3b 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -31,60 +31,19 @@ from pathlib import Path from typing import Any, Dict, List import pandas as pd -import pytz -from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data +from freqtrade.data.btanalysis import load_trades from freqtrade.plot.plotting import generate_graph, generate_plot_file from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration -from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) _CONF: Dict[str, Any] = {} -timeZone = pytz.UTC - - -def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: - """ - Load trades, either from a DB (using dburl) or via a backtest export file. - :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) - :param exportfilename: Path to a file exported from backtesting - :returns: Dataframe containing Trades - """ - # TODO: Document and move to btanalysis - trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) - - if db_url: - persistence.init(db_url, clean_open_orders=False) - columns = ["pair", "profit", "open_time", "close_time", - "open_rate", "close_rate", "duration"] - - for x in Trade.query.all(): - logger.info("date: {}".format(x.open_date)) - - trades = pd.DataFrame([(t.pair, t.calc_profit(), - t.open_date.replace(tzinfo=timeZone), - t.close_date.replace(tzinfo=timeZone) if t.close_date else None, - t.open_rate, t.close_rate, - t.close_date.timestamp() - t.open_date.timestamp() - if t.close_date else None) - for t in Trade.query.all()], - columns=columns) - - elif exportfilename: - - file = Path(exportfilename) - if file.exists(): - trades = load_backtest_data(file) - - return trades - def get_trading_env(args: Namespace): """ From 1c53aa5687c7b714b5bd7043a3e977f82e9f5ea2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:57:21 +0200 Subject: [PATCH 51/92] Add tests for load_trades --- freqtrade/tests/data/test_btanalysis.py | 28 ++++++++- freqtrade/tests/test_persistence.py | 84 +++++++++++++------------ 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index dd7cbe0d9..4a8babf1d 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -1,8 +1,11 @@ import pytest +from unittest.mock import MagicMock from pandas import DataFrame -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data +from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data, load_trades from freqtrade.data.history import make_testdata_path +from freqtrade.persistence import init, Trade +from freqtrade.tests.test_persistence import init_persistence, create_mock_trades def test_load_backtest_data(): @@ -19,3 +22,26 @@ def test_load_backtest_data(): with pytest.raises(ValueError, match=r"File .* does not exist\."): load_backtest_data(str("filename") + "nofile") + + +def test_load_trades_file(default_conf, fee, mocker): + # Real testing of load_backtest_data is done in test_load_backtest_data + lbt = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock()) + filename = make_testdata_path(None) / "backtest-result_test.json" + load_trades(db_url=None, exportfilename=filename) + assert lbt.call_count == 1 + + +@pytest.mark.usefixtures("init_persistence") +def test_load_trades_db(default_conf, fee, mocker): + + create_mock_trades(fee) + # remove init so it does not init again + init_mock = mocker.patch('freqtrade.persistence.init', MagicMock()) + + trades = load_trades(db_url=default_conf['db_url'], exportfilename=None) + assert init_mock.call_count == 1 + assert len(trades) == 3 + assert isinstance(trades, DataFrame) + assert "pair" in trades.columns + assert "open_time" in trades.columns diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index bb00fa8f4..381f04bd1 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -16,6 +16,50 @@ def init_persistence(default_conf): init(default_conf['db_url'], default_conf['dry_run']) +def create_mock_trades(fee): + """ + Create some fake trades ... + """ + # Simulate dry_run entries + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='dry_run_buy_12345' + ) + Trade.session.add(trade) + + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + is_open=False, + open_order_id='dry_run_sell_12345' + ) + Trade.session.add(trade) + + # Simulate prod entry + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='prod_buy_12345' + ) + Trade.session.add(trade) + + def test_init_create_session(default_conf): # Check if init create a session init(default_conf['db_url'], default_conf['dry_run']) @@ -671,45 +715,7 @@ def test_adjust_min_max_rates(fee): @pytest.mark.usefixtures("init_persistence") def test_get_open(default_conf, fee): - # Simulate dry_run entries - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - open_order_id='dry_run_buy_12345' - ) - Trade.session.add(trade) - - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - is_open=False, - open_order_id='dry_run_sell_12345' - ) - Trade.session.add(trade) - - # Simulate prod entry - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - open_order_id='prod_buy_12345' - ) - Trade.session.add(trade) - + create_mock_trades(fee) assert len(Trade.get_open_trades()) == 2 From 1cd84157230c4416a106215460f7da8a0192cb28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 11:12:19 +0200 Subject: [PATCH 52/92] Move extract_trades_of_period to btanlaysis --- freqtrade/data/btanalysis.py | 10 +++++ freqtrade/tests/data/test_btanalysis.py | 53 ++++++++++++++++++++++--- scripts/plot_dataframe.py | 14 +------ 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index fb120e521..e1ddd1638 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -108,3 +108,13 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: trades = load_backtest_data(file) return trades + + +def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: + """ + Compare trades and backtested pair DataFrames to get trades performed on backtested period + :return: the DataFrame of a trades of period + """ + trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & + (trades['close_time'] <= dataframe.iloc[-1]['date'])] + return trades diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 4a8babf1d..aa066557b 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -1,11 +1,18 @@ -import pytest from unittest.mock import MagicMock -from pandas import DataFrame -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data, load_trades -from freqtrade.data.history import make_testdata_path -from freqtrade.persistence import init, Trade -from freqtrade.tests.test_persistence import init_persistence, create_mock_trades +from arrow import Arrow +import pytest +from pandas import DataFrame, to_datetime + +from freqtrade.arguments import Arguments, TimeRange +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, + extract_trades_of_period, + load_backtest_data, load_trades) +from freqtrade.data.history import load_pair_history, make_testdata_path +from freqtrade.persistence import Trade, init +from freqtrade.strategy.interface import SellType +from freqtrade.tests.test_persistence import (create_mock_trades, + init_persistence) def test_load_backtest_data(): @@ -45,3 +52,37 @@ def test_load_trades_db(default_conf, fee, mocker): assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_time" in trades.columns + + +def test_extract_trades_of_period(): + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + + data = load_pair_history(pair=pair, ticker_interval='1m', + datadir=None, timerange=timerange) + + # timerange = 2017-11-14 06:07 - 2017-11-14 22:58:00 + trades = DataFrame( + {'pair': [pair, pair, pair, pair], + 'profit_percent': [0.0, 0.1, -0.2, -0.5], + 'profit_abs': [0.0, 1, -2, -5], + 'open_time': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime, + Arrow(2017, 11, 14, 9, 41, 0).datetime, + Arrow(2017, 11, 14, 14, 20, 0).datetime, + Arrow(2017, 11, 15, 3, 40, 0).datetime, + ], utc=True + ), + 'close_time': to_datetime([Arrow(2017, 11, 13, 16, 40, 0).datetime, + Arrow(2017, 11, 14, 10, 41, 0).datetime, + Arrow(2017, 11, 14, 15, 25, 0).datetime, + Arrow(2017, 11, 15, 3, 55, 0).datetime, + ], utc=True) + }) + trades1 = extract_trades_of_period(data, trades) + # First and last trade are dropped as they are out of range + assert len(trades1) == 2 + assert trades1.iloc[0].open_time == Arrow(2017, 11, 14, 9, 41, 0).datetime + assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime + assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime + assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime + diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 005148d3b..6d2e545ce 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -35,9 +35,10 @@ import pandas as pd from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_trades -from freqtrade.plot.plotting import generate_graph, generate_plot_file from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration +from freqtrade.plot.plotting import (extract_trades_of_period, generate_graph, + generate_plot_file) from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -119,17 +120,6 @@ def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: return dataframe -def extract_trades_of_period(dataframe, trades) -> pd.DataFrame: - """ - Compare trades and backtested pair DataFrames to get trades performed on backtested period - :return: the DataFrame of a trades of period - """ - # TODO: Document and move to btanalysis (?) - trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & - (trades['close_time'] <= dataframe.iloc[-1]['date'])] - return trades - - def analyse_and_plot_pairs(args: Namespace): """ From arguments provided in cli: From bf2c0390e7cc01020dbd10d2990a3606f75adaa1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 12:54:27 +0200 Subject: [PATCH 53/92] Adjust some imports --- freqtrade/tests/conftest.py | 5 +++++ freqtrade/tests/data/test_btanalysis.py | 8 ++------ freqtrade/tests/test_persistence.py | 5 ----- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 034cb5f8b..e956d89c4 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -151,6 +151,11 @@ def patch_coinmarketcap(mocker) -> None: ) +@pytest.fixture(scope='function') +def init_persistence(default_conf): + persistence.init(default_conf['db_url'], default_conf['dry_run']) + + @pytest.fixture(scope="function") def default_conf(): """ Returns validated configuration suitable for most tests """ diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index aa066557b..6fa529394 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -4,15 +4,12 @@ from arrow import Arrow import pytest from pandas import DataFrame, to_datetime -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, extract_trades_of_period, load_backtest_data, load_trades) from freqtrade.data.history import load_pair_history, make_testdata_path -from freqtrade.persistence import Trade, init -from freqtrade.strategy.interface import SellType -from freqtrade.tests.test_persistence import (create_mock_trades, - init_persistence) +from freqtrade.tests.test_persistence import create_mock_trades def test_load_backtest_data(): @@ -85,4 +82,3 @@ def test_extract_trades_of_period(): assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime - diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 381f04bd1..32425ef7b 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -11,11 +11,6 @@ from freqtrade.persistence import Trade, clean_dry_run_db, init from freqtrade.tests.conftest import log_has -@pytest.fixture(scope='function') -def init_persistence(default_conf): - init(default_conf['db_url'], default_conf['dry_run']) - - def create_mock_trades(fee): """ Create some fake trades ... From 0300128cb82f4a109902ac78770345474a41793a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 19:35:15 +0200 Subject: [PATCH 54/92] Move plot-options to arguments.py --- freqtrade/arguments.py | 32 ++++++++++++++++++++++++++++++- freqtrade/tests/test_arguments.py | 14 ++++++++++++++ scripts/plot_dataframe.py | 30 +++-------------------------- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 35d388432..fde372b63 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -55,7 +55,7 @@ class Arguments(object): # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) - if parsed_arg.config is None and not no_default_config: + if not no_default_config and parsed_arg.config is None: parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg @@ -514,3 +514,33 @@ class Arguments(object): dest='erase', action='store_true' ) + + def plot_dataframe_options(self) -> None: + """ + Parses given arguments for plot_dataframe + """ + self.parser.add_argument( + '--indicators1', + help='Set indicators from your strategy you want in the first row of the graph. Separate ' + 'them with a comma. E.g: ema3,ema5 (default: %(default)s)', + type=str, + default='sma,ema3,ema5', + dest='indicators1', + ) + + self.parser.add_argument( + '--indicators2', + help='Set indicators from your strategy you want in the third row of the graph. Separate ' + 'them with a comma. E.g: macd,fastd,fastk (default: %(default)s)', + type=str, + default='macd,macdsignal', + dest='indicators2', + ) + self.parser.add_argument( + '--plot-limit', + help='Specify tick limit for plotting - too high values cause huge files - ' + 'Default: %(default)s', + dest='plot_limit', + default=750, + type=int, + ) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index afa42f287..d1b96a923 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -186,6 +186,20 @@ def test_download_data_options() -> None: assert args.exchange == 'binance' +def test_plot_dataframe_options() -> None: + args = [ + '--indicators1', 'sma10,sma100', + '--indicators2', 'macd,fastd,fastk', + '--plot-limit', '30', + ] + arguments = Arguments(args, '') + arguments.plot_dataframe_options() + args = arguments.parse_args(True) + assert args.indicators1 == "sma10,sma100" + assert args.indicators2 == "macd,fastd,fastk" + assert args.plot_limit == 30 + + def test_check_int_positive() -> None: assert Arguments.check_int_positive("3") == 3 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 6d2e545ce..d37d63c32 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -34,10 +34,10 @@ import pandas as pd from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades +from freqtrade.data.btanalysis import load_trades, extract_trades_of_period from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import (extract_trades_of_period, generate_graph, +from freqtrade.plot.plotting import (generate_graph, generate_plot_file) from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -171,31 +171,7 @@ def plot_parse_args(args: List[str]) -> Namespace: """ arguments = Arguments(args, 'Graph dataframe') arguments.scripts_options() - arguments.parser.add_argument( - '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', - type=str, - default='sma,ema3,ema5', - dest='indicators1', - ) - - arguments.parser.add_argument( - '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a coma. E.g: fastd,fastk (default: %(default)s)', - type=str, - default='macd,macdsignal', - dest='indicators2', - ) - arguments.parser.add_argument( - '--plot-limit', - help='Specify tick limit for plotting - too high values cause huge files - ' - 'Default: %(default)s', - dest='plot_limit', - default=750, - type=int, - ) + arguments.plot_dataframe_options() arguments.common_args_parser() arguments.optimizer_shared_options(arguments.parser) arguments.backtesting_options(arguments.parser) From 3f04930f383272f588bd33d455b10f832cf16c18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 13:19:06 +0200 Subject: [PATCH 55/92] Require pairs argument --- freqtrade/arguments.py | 7 +++++++ freqtrade/tests/test_arguments.py | 17 +++++++++++++---- scripts/plot_dataframe.py | 5 +++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index fde372b63..cef38784d 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -519,6 +519,13 @@ class Arguments(object): """ Parses given arguments for plot_dataframe """ + self.parser.add_argument( + '-p', '--pairs', + help='Show profits for only this pairs. Pairs are comma-separated.', + dest='pairs', + required=True, + default=None + ) self.parser.add_argument( '--indicators1', help='Set indicators from your strategy you want in the first row of the graph. Separate ' diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index d1b96a923..d584a9e01 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -191,13 +191,22 @@ def test_plot_dataframe_options() -> None: '--indicators1', 'sma10,sma100', '--indicators2', 'macd,fastd,fastk', '--plot-limit', '30', + '-p', 'UNITTEST/BTC', ] arguments = Arguments(args, '') arguments.plot_dataframe_options() - args = arguments.parse_args(True) - assert args.indicators1 == "sma10,sma100" - assert args.indicators2 == "macd,fastd,fastk" - assert args.plot_limit == 30 + pargs = arguments.parse_args(True) + assert pargs.indicators1 == "sma10,sma100" + assert pargs.indicators2 == "macd,fastd,fastk" + assert pargs.plot_limit == 30 + assert pargs.pairs == "UNITTEST/BTC" + + # Pop pairs argument + args = args[:-2] + arguments = Arguments(args, '') + arguments.plot_dataframe_options() + with pytest.raises(SystemExit, match=r'2'): + arguments.parse_args(True) def test_check_int_positive() -> None: diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index d37d63c32..c44f2aa4b 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -89,7 +89,7 @@ def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, ticker_interval=ticker_interval, refresh_pairs=_CONF.get('refresh_pairs', False), timerange=timerange, - exchange=Exchange(_CONF), + exchange=exchange, live=live, ) @@ -132,6 +132,8 @@ def analyse_and_plot_pairs(args: Namespace): :return: None """ strategy, exchange, pairs = get_trading_env(args) + pairs = args.pairs.split(',') + # Set timerange to use timerange = Arguments.parse_timerange(args.timerange) ticker_interval = strategy.ticker_interval @@ -170,7 +172,6 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph dataframe') - arguments.scripts_options() arguments.plot_dataframe_options() arguments.common_args_parser() arguments.optimizer_shared_options(arguments.parser) From 907c2f1e6b5a8ed53649640d09f62b5939d71236 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 13:31:24 +0200 Subject: [PATCH 56/92] Copy plot options to config --- freqtrade/configuration.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 82d96313d..c63e99318 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -98,6 +98,9 @@ class Configuration(object): # Load Optimize configurations config = self._load_optimize_config(config) + # Add plotting options if available + config = self._load_plot_config(config) + # Set runmode if not self.runmode: # Handle real mode, infer dry/live from config @@ -332,6 +335,26 @@ class Configuration(object): return config + def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Extract information for sys.argv Plotting configuration + :return: configuration as dictionary + """ + + self._args_to_config(config, argname='pairs', + logstring='Using pairs {}') + + self._args_to_config(config, argname='indicators1', + logstring='Using indicators1: {}') + + self._args_to_config(config, argname='indicators2', + logstring='Using indicators2: {}') + + self._args_to_config(config, argname='plot_limit', + logstring='Limiting plot to: {}') + + return config + def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: """ Validate the configuration follow the Config Schema From 488bb971ffa25a6a277e4d494b098401d9c9d870 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 13:41:36 +0200 Subject: [PATCH 57/92] Get rid of global conf object --- scripts/plot_dataframe.py | 70 ++++++++++++++------------------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index c44f2aa4b..e54a78124 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -35,47 +35,17 @@ import pandas as pd from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_trades, extract_trades_of_period -from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (generate_graph, generate_plot_file) -from freqtrade.resolvers import StrategyResolver +from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) -_CONF: Dict[str, Any] = {} -def get_trading_env(args: Namespace): - """ - Initalize freqtrade Exchange and Strategy, split pairs recieved in parameter - :return: Strategy - """ - global _CONF - - # Load the configuration - _CONF.update(setup_configuration(args, RunMode.BACKTEST)) - - pairs = args.pairs.split(',') - if pairs is None: - logger.critical('Parameter --pairs mandatory;. E.g --pairs ETH/BTC,XRP/BTC') - exit() - - # Load the strategy - try: - strategy = StrategyResolver(_CONF).strategy - exchange = Exchange(_CONF) - except AttributeError: - logger.critical( - 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"', - args.strategy - ) - exit() - - return [strategy, exchange, pairs] - - -def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, live: bool): +def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, + datadir: Path, refresh_pairs: bool, live: bool): """ Get tickers data for each pairs on live or local, option defined in args :return: dictionary of tickers. output format: {'pair': tickersdata} @@ -84,10 +54,10 @@ def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, ticker_interval = strategy.ticker_interval tickers = history.load_data( - datadir=Path(str(_CONF.get("datadir"))), + datadir=datadir, pairs=pairs, ticker_interval=ticker_interval, - refresh_pairs=_CONF.get('refresh_pairs', False), + refresh_pairs=refresh_pairs, timerange=timerange, exchange=exchange, live=live, @@ -120,7 +90,7 @@ def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: return dataframe -def analyse_and_plot_pairs(args: Namespace): +def analyse_and_plot_pairs(config: Dict[str, Any]): """ From arguments provided in cli: -Initialise backtest env @@ -131,14 +101,20 @@ def analyse_and_plot_pairs(args: Namespace): -Generate plot files :return: None """ - strategy, exchange, pairs = get_trading_env(args) - pairs = args.pairs.split(',') + exchange_name = config.get('exchange', {}).get('name').title() + exchange = ExchangeResolver(exchange_name, config).exchange + + strategy = StrategyResolver(config).strategy + pairs = config["pairs"].split(',') # Set timerange to use - timerange = Arguments.parse_timerange(args.timerange) + timerange = Arguments.parse_timerange(config["timerange"]) ticker_interval = strategy.ticker_interval - tickers = get_tickers_data(strategy, exchange, pairs, timerange, args.live) + tickers = get_tickers_data(strategy, exchange, pairs, timerange, + datadir=Path(str(config.get("datadir"))), + refresh_pairs=config.get('refresh_pairs', False), + live=config.get("live", False)) pair_counter = 0 for pair, data in tickers.items(): pair_counter += 1 @@ -147,8 +123,8 @@ def analyse_and_plot_pairs(args: Namespace): tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - trades = load_trades(db_url=args.db_url, - exportfilename=args.exportfilename) + trades = load_trades(db_url=config["db_url"], + exportfilename=config["exportfilename"]) trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) @@ -156,8 +132,8 @@ def analyse_and_plot_pairs(args: Namespace): pair=pair, data=dataframe, trades=trades, - indicators1=args.indicators1.split(","), - indicators2=args.indicators2.split(",") + indicators1=config["indicators1"].split(","), + indicators2=config["indicators2"].split(",") ) generate_plot_file(fig, pair, ticker_interval) @@ -176,7 +152,11 @@ def plot_parse_args(args: List[str]) -> Namespace: arguments.common_args_parser() arguments.optimizer_shared_options(arguments.parser) arguments.backtesting_options(arguments.parser) - return arguments.parse_args() + parsed_args = arguments.parse_args() + + # Load the configuration + config = setup_configuration(parsed_args, RunMode.BACKTEST) + return config def main(sysargv: List[str]) -> None: From 4b7dfc64c66afa33f2723be1214668b16f42f327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 14:03:55 +0200 Subject: [PATCH 58/92] Add test for generate_plot_file --- freqtrade/plot/plotting.py | 1 - freqtrade/tests/test_plotting.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index b1e32c4fb..dcea3291f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,7 +1,6 @@ import logging from typing import List -import arrow import pandas as pd from pathlib import Path diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index e264ef6b3..b1dad5d5c 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -32,6 +32,7 @@ def generage_empty_figure(): vertical_spacing=0.0001, ) + def test_generate_row(default_conf, caplog): pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) @@ -155,3 +156,14 @@ def test_generate_graph_no_trades(default_conf, mocker): assert row_mock.call_count == 2 assert trades_mock.call_count == 1 + + +def test_generate_plot_file(mocker, caplog): + fig = generage_empty_figure() + plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) + generate_plot_file(fig, "UNITTEST/BTC", "5m") + + assert plot_mock.call_count == 1 + assert plot_mock.call_args[0][0] == fig + assert (plot_mock.call_args_list[0][1]['filename'] + == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") From fc3e3c468c3a79dacb05259f8b4c2cdeedfb3212 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 19:26:43 +0200 Subject: [PATCH 59/92] File existence is checked in load_backtest_data --- freqtrade/data/btanalysis.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index e1ddd1638..4aecf8ecf 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -103,9 +103,7 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: elif exportfilename: - file = Path(exportfilename) - if file.exists(): - trades = load_backtest_data(file) + trades = load_backtest_data(Path(exportfilename)) return trades From 0eb109f8f7a01d8962f76cd89a111c9aa8ab3a7a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 19:53:48 +0200 Subject: [PATCH 60/92] Improve some tests --- freqtrade/arguments.py | 8 ++++---- freqtrade/tests/test_plotting.py | 23 +++++++++++++++++++++-- scripts/plot_dataframe.py | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index cef38784d..3f21709c9 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -528,8 +528,8 @@ class Arguments(object): ) self.parser.add_argument( '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a comma. E.g: ema3,ema5 (default: %(default)s)', + help='Set indicators from your strategy you want in the first row of the graph. ' + 'Separate them with a comma. E.g: ema3,ema5 (default: %(default)s)', type=str, default='sma,ema3,ema5', dest='indicators1', @@ -537,8 +537,8 @@ class Arguments(object): self.parser.add_argument( '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a comma. E.g: macd,fastd,fastk (default: %(default)s)', + help='Set indicators from your strategy you want in the third row of the graph. ' + 'Separate them with a comma. E.g: macd,fastd,fastk (default: %(default)s)', type=str, default='macd,macdsignal', dest='indicators2', diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index b1dad5d5c..15ab698d8 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,8 +5,9 @@ from plotly import tools import plotly.graph_objs as go from copy import deepcopy -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import TimeRange from freqtrade.data import history +from freqtrade.data.btanalysis import load_backtest_data from freqtrade.plot.plotting import (generate_graph, generate_plot_file, generate_row, plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy @@ -71,8 +72,26 @@ def test_plot_trades(): # nothing happens when no trades are available fig = plot_trades(fig1, None) assert fig == fig1 + pair = "ADA/BTC" + filename = history.make_testdata_path(None) / "backtest-result_test.json" + trades = load_backtest_data(filename) + trades = trades.loc[trades['pair'] == pair] - # TODO: implement tests that do something + fig = plot_trades(fig, trades) + figure = fig1.layout.figure + + # Check buys - color, should be in first graph, ... + trade_buy = find_trace_in_fig_data(figure.data, "trade_buy") + assert isinstance(trade_buy, go.Scatter) + assert trade_buy.yaxis == 'y' + assert len(trades) == len(trade_buy.x) + assert trade_buy.marker.color == 'green' + + trade_sell = find_trace_in_fig_data(figure.data, "trade_sell") + assert isinstance(trade_sell, go.Scatter) + assert trade_sell.yaxis == 'y' + assert len(trades) == len(trade_sell.x) + assert trade_sell.marker.color == 'red' def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index e54a78124..54ce199f4 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -141,7 +141,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): logger.info('End of ploting process %s plots generated', pair_counter) -def plot_parse_args(args: List[str]) -> Namespace: +def plot_parse_args(args: List[str]) -> Dict[str, Any]: """ Parse args passed to the script :param args: Cli arguments From 765eff23f018c6532fa169505d354f175f0f08b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 20:14:31 +0200 Subject: [PATCH 61/92] Fix typo --- freqtrade/plot/plotting.py | 2 +- scripts/plot_dataframe.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index dcea3291f..94c0830bf 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -93,7 +93,7 @@ def generate_graph( ) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB - Volume will always be ploted in row2, so Row 1 and are to our disposal for custom indicators + Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators :param pair: Pair to Display on the graph :param data: OHLCV DataFrame containing indicators and buy/sell signals :param trades: All trades created diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 54ce199f4..eebe20be2 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -26,7 +26,6 @@ Example of usage: """ import logging import sys -from argparse import Namespace from pathlib import Path from typing import Any, Dict, List From 813c008af22d78a854c11f6dd04cda50e579b325 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 16 Jun 2019 21:37:43 +0300 Subject: [PATCH 62/92] setup_configuration() cleanup --- freqtrade/optimize/__init__.py | 10 +++------- freqtrade/utils.py | 5 +++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 475aaa82f..8b548eefe 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -5,8 +5,9 @@ from typing import Any, Dict from filelock import FileLock, Timeout from freqtrade import DependencyException, constants -from freqtrade.configuration import Configuration from freqtrade.state import RunMode +from freqtrade.utils import setup_utils_configuration + logger = logging.getLogger(__name__) @@ -17,12 +18,7 @@ def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: :param args: Cli args from Arguments() :return: Configuration """ - configuration = Configuration(args, method) - config = configuration.load_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' + config = setup_utils_configuration(args, method) if method == RunMode.BACKTEST: if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 324b54a4e..d550ef43c 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -10,15 +10,16 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: +def setup_utils_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: """ - Prepare the configuration for the Hyperopt module + Prepare the configuration for utils subcommands :param args: Cli args from Arguments() :return: Configuration """ configuration = Configuration(args, method) config = configuration.load_config() + config['exchange']['dry_run'] = True # Ensure we do not use Exchange credentials config['exchange']['key'] = '' config['exchange']['secret'] = '' From 195bf5a4ccdbfffb3c997e64332af7177f1ef9da Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 16 Jun 2019 21:37:59 +0300 Subject: [PATCH 63/92] tests adjusted --- freqtrade/tests/test_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index 7550efb23..a12b709d7 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -1,17 +1,18 @@ -from freqtrade.utils import setup_configuration, start_list_exchanges +from freqtrade.utils import setup_utils_configuration, start_list_exchanges from freqtrade.tests.conftest import get_args from freqtrade.state import RunMode import re -def test_setup_configuration(): +def test_setup_utils_configuration(): args = [ '--config', 'config.json.example', ] - config = setup_configuration(get_args(args), RunMode.OTHER) + config = setup_utils_configuration(get_args(args), RunMode.OTHER) assert "exchange" in config + assert config['exchange']['dry_run'] is True assert config['exchange']['key'] == '' assert config['exchange']['secret'] == '' From d217f32bbcde1b751ea45eceed79f50d267dfd71 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 17 Jun 2019 04:35:39 +0300 Subject: [PATCH 64/92] minor: fix typo in freqtradebot.py --- freqtrade/freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 471e9d218..e6eb74c21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -205,19 +205,19 @@ class FreqtradeBot(object): else: stake_amount = self.config['stake_amount'] - avaliable_amount = self.wallets.get_free(self.config['stake_currency']) + available_amount = self.wallets.get_free(self.config['stake_currency']) if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: open_trades = len(Trade.get_open_trades()) if open_trades >= self.config['max_open_trades']: logger.warning('Can\'t open a new trade: max number of trades is reached') return None - return avaliable_amount / (self.config['max_open_trades'] - open_trades) + return available_amount / (self.config['max_open_trades'] - open_trades) # Check if stake_amount is fulfilled - if avaliable_amount < stake_amount: + if available_amount < stake_amount: raise DependencyException( - f"Available balance({avaliable_amount} {self.config['stake_currency']}) is " + f"Available balance({available_amount} {self.config['stake_currency']}) is " f"lower than stake amount({stake_amount} {self.config['stake_currency']})" ) From 475e76b272593ab9bd96db00d8ca3173bf57d978 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 06:55:30 +0200 Subject: [PATCH 65/92] Add order_type to buy_notification --- freqtrade/rpc/telegram.py | 2 +- freqtrade/tests/rpc/test_rpc_telegram.py | 6 ++++-- freqtrade/tests/rpc/test_rpc_webhook.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 497c117ac..41d3ca0e9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -132,7 +132,7 @@ class Telegram(RPC): msg['stake_amount_fiat'] = 0 message = ("*{exchange}:* Buying {pair}\n" - "with limit `{limit:.8f}\n" + "at rate `{limit:.8f}\n" "({stake_amount:.6f} {stake_currency}").format(**msg) if msg.get('fiat_currency', None): diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 46ef15f56..f1615d0d5 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1188,6 +1188,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 1.099e-05, + 'order_type': 'limit', 'stake_amount': 0.001, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', @@ -1195,7 +1196,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == '*Bittrex:* Buying ETH/BTC\n' \ - 'with limit `0.00001099\n' \ + 'at rate `0.00001099\n' \ '(0.001000 BTC,0.000 USD)`' @@ -1339,6 +1340,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 1.099e-05, + 'order_type': 'limit', 'stake_amount': 0.001, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', @@ -1346,7 +1348,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == '*Bittrex:* Buying ETH/BTC\n' \ - 'with limit `0.00001099\n' \ + 'at rate `0.00001099\n' \ '(0.001000 BTC)`' diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py index da7aec0a6..a9129529c 100644 --- a/freqtrade/tests/rpc/test_rpc_webhook.py +++ b/freqtrade/tests/rpc/test_rpc_webhook.py @@ -126,6 +126,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 0.005, + 'order_type': 'limit', 'stake_amount': 0.8, 'stake_amount_fiat': 500, 'stake_currency': 'BTC', From 557122921a876044aedccdf3ee54335d7e9b69c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 06:55:54 +0200 Subject: [PATCH 66/92] Add order_type to sell-notification --- freqtrade/freqtradebot.py | 7 ++++--- freqtrade/tests/rpc/test_rpc_telegram.py | 6 ++++++ freqtrade/tests/rpc/test_rpc_webhook.py | 1 + freqtrade/tests/test_freqtradebot.py | 5 +++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e6eb74c21..305a97ba6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -345,8 +345,8 @@ class FreqtradeBot(object): return False amount = stake_amount / buy_limit_requested - - order = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], + order_type = self.strategy.order_types['buy'] + order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) order_id = order['id'] @@ -356,7 +356,6 @@ class FreqtradeBot(object): buy_limit_filled_price = buy_limit_requested if order_status == 'expired' or order_status == 'rejected': - order_type = self.strategy.order_types['buy'] order_tif = self.strategy.order_time_in_force['buy'] # return false if the order is not filled @@ -390,6 +389,7 @@ class FreqtradeBot(object): 'exchange': self.exchange.name.capitalize(), 'pair': pair_s, 'limit': buy_limit_filled_price, + 'order_type': order_type, 'stake_amount': stake_amount, 'stake_currency': stake_currency, 'fiat_currency': fiat_currency @@ -875,6 +875,7 @@ class FreqtradeBot(object): 'pair': trade.pair, 'gain': gain, 'limit': trade.close_rate_requested, + 'order_type': self.strategy.order_types['sell'], 'amount': trade.amount, 'open_rate': trade.open_rate, 'current_rate': current_rate, diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index f1615d0d5..9bc2ba06e 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -756,6 +756,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, 'gain': 'profit', 'limit': 1.172e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.172e-05, 'profit_amount': 6.126e-05, @@ -810,6 +811,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, 'gain': 'loss', 'limit': 1.044e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -5.492e-05, @@ -855,6 +857,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker 'gain': 'loss', 'limit': 1.098e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.098e-05, 'profit_amount': -5.91e-06, @@ -1218,6 +1221,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, + 'order_type': 'market', 'open_rate': 7.5e-05, 'current_rate': 3.201e-05, 'profit_amount': -0.05746268, @@ -1243,6 +1247,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, + 'order_type': 'market', 'open_rate': 7.5e-05, 'current_rate': 3.201e-05, 'profit_amount': -0.05746268, @@ -1369,6 +1374,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, + 'order_type': 'limit', 'open_rate': 7.5e-05, 'current_rate': 3.201e-05, 'profit_amount': -0.05746268, diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py index a9129529c..a2dcd9b31 100644 --- a/freqtrade/tests/rpc/test_rpc_webhook.py +++ b/freqtrade/tests/rpc/test_rpc_webhook.py @@ -74,6 +74,7 @@ def test_send_msg(default_conf, mocker): 'gain': "profit", 'limit': 0.005, 'amount': 0.8, + 'order_type': 'limit', 'open_rate': 0.004, 'current_rate': 0.005, 'profit_amount': 0.001, diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 6566e4036..ceb695423 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1994,6 +1994,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc 'gain': 'profit', 'limit': 1.172e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.172e-05, 'profit_amount': 6.126e-05, @@ -2040,6 +2041,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, 'gain': 'loss', 'limit': 1.044e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -5.492e-05, @@ -2094,6 +2096,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'gain': 'loss', 'limit': 1.08801e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -1.498e-05, @@ -2265,6 +2268,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, 'gain': 'profit', 'limit': 1.172e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.172e-05, 'profit_amount': 6.126e-05, @@ -2312,6 +2316,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, 'gain': 'loss', 'limit': 1.044e-05, 'amount': 90.99181073703367, + 'order_type': 'market', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -5.492e-05, From 06afb3f155420c85221998f7e3b41b8a609147cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 07:01:17 +0200 Subject: [PATCH 67/92] Don't use "limit" for sell-orders either --- freqtrade/rpc/telegram.py | 2 +- freqtrade/tests/rpc/test_rpc_telegram.py | 6 +++--- freqtrade/tests/test_freqtradebot.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 41d3ca0e9..3eb060074 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -144,7 +144,7 @@ class Telegram(RPC): msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) message = ("*{exchange}:* Selling {pair}\n" - "*Limit:* `{limit:.8f}`\n" + "*Rate:* `{limit:.8f}`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 9bc2ba06e..b34e214af 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1232,7 +1232,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == ('*Binance:* Selling KEY/ETH\n' - '*Limit:* `0.00003201`\n' + '*Rate:* `0.00003201`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1257,7 +1257,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == ('*Binance:* Selling KEY/ETH\n' - '*Limit:* `0.00003201`\n' + '*Rate:* `0.00003201`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1385,7 +1385,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == '*Binance:* Selling KEY/ETH\n' \ - '*Limit:* `0.00003201`\n' \ + '*Rate:* `0.00003201`\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00007500`\n' \ '*Current Rate:* `0.00003201`\n' \ diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index ceb695423..65225689b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2316,7 +2316,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, 'gain': 'loss', 'limit': 1.044e-05, 'amount': 90.99181073703367, - 'order_type': 'market', + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -5.492e-05, From 7cd36239a461b332bc53699c3a66a17a0ca1a968 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 07:03:33 +0200 Subject: [PATCH 68/92] UPdate documentation with new value --- docs/webhook-config.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 811b57f9b..112f8a77e 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -43,6 +43,7 @@ Possible parameters are: * `stake_amount` * `stake_currency` * `fiat_currency` +* `order_type` ### Webhooksell @@ -61,6 +62,7 @@ Possible parameters are: * `stake_currency` * `fiat_currency` * `sell_reason` +* `order_type` ### Webhookstatus From ba4890d303348f29af540bfb9dff05d6f9f52f65 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 14:34:45 +0200 Subject: [PATCH 69/92] Fix tests on windows --- freqtrade/tests/conftest.py | 3 ++- freqtrade/tests/strategy/test_strategy.py | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 034cb5f8b..7424d5ece 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -5,6 +5,7 @@ import re from copy import deepcopy from datetime import datetime from functools import reduce +from pathlib import Path from typing import List from unittest.mock import MagicMock, PropertyMock @@ -860,7 +861,7 @@ def tickers(): @pytest.fixture def result(): - with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: + with Path('freqtrade/tests/testdata/UNITTEST_BTC-1m.json').open('r') as data_file: return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True) # FIX: diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index b96f9c79e..15d1c18ef 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -75,14 +75,10 @@ def test_load_strategy_byte64(result): def test_load_strategy_invalid_directory(result, caplog): resolver = StrategyResolver() - extra_dir = path.join('some', 'path') + extra_dir = Path.cwd() / 'some/path' resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) - assert ( - 'freqtrade.resolvers.strategy_resolver', - logging.WARNING, - 'Path "{}" does not exist'.format(extra_dir), - ) in caplog.record_tuples + assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) From 25755f6adfb2e4307ed478ff9fbf4ab0dd051a20 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 17 Jun 2019 15:23:13 +0000 Subject: [PATCH 70/92] Update ccxt from 1.18.667 to 1.18.725 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 8b44e9d28..ed3f052a9 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.667 +ccxt==1.18.725 SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 arrow==0.14.2 From 6973087d5b2dad8e74263eaeebf86cd17bf631a1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 17 Jun 2019 15:23:14 +0000 Subject: [PATCH 71/92] Update pytest from 4.6.2 to 4.6.3 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 315033847..a8ba6f6c0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.6.2 +pytest==4.6.3 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From 0e7ea1dadad4d57eb355b0fa799e04b973596882 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 17 Jun 2019 15:23:15 +0000 Subject: [PATCH 72/92] Update coveralls from 1.8.0 to 1.8.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a8ba6f6c0..21bb899b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,5 +8,5 @@ pytest==4.6.3 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 -coveralls==1.8.0 +coveralls==1.8.1 mypy==0.701 From 6f950bbd66eb02a2a99df48dd791999a483679f8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 18 Jun 2019 01:46:30 +0300 Subject: [PATCH 73/92] json validator cosmetics --- freqtrade/configuration.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 82d96313d..01bbb955d 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -34,13 +34,17 @@ def set_loggers(log_level: int = 0) -> None: logging.getLogger('telegram').setLevel(logging.INFO) -def _extend_with_default(validator_class): - validate_properties = validator_class.VALIDATORS["properties"] +def _extend_validator(validator_class): + """ + Extended validator for the Freqtrade configuration JSON Schema. + Currently it only handles defaults for subschemas. + """ + validate_properties = validator_class.VALIDATORS['properties'] def set_defaults(validator, properties, instance, schema): for prop, subschema in properties.items(): - if "default" in subschema: - instance.setdefault(prop, subschema["default"]) + if 'default' in subschema: + instance.setdefault(prop, subschema['default']) for error in validate_properties( validator, properties, instance, schema, @@ -48,11 +52,11 @@ def _extend_with_default(validator_class): yield error return validators.extend( - validator_class, {"properties": set_defaults}, + validator_class, {'properties': set_defaults} ) -ValidatorWithDefaults = _extend_with_default(Draft4Validator) +FreqtradeValidator = _extend_validator(Draft4Validator) class Configuration(object): @@ -339,7 +343,7 @@ class Configuration(object): :return: Returns the config if valid, otherwise throw an exception """ try: - ValidatorWithDefaults(constants.CONF_SCHEMA).validate(conf) + FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) return conf except ValidationError as exception: logger.critical( From 8c40a406b6830e13bbc76a96c51ca57fa5b17c10 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 19 Jun 2019 01:53:38 +0300 Subject: [PATCH 74/92] arguments cleanup --- freqtrade/arguments.py | 174 +++++++++++++++--------------- freqtrade/tests/test_arguments.py | 4 +- scripts/download_backtest_data.py | 3 +- scripts/plot_dataframe.py | 35 ++---- scripts/plot_profit.py | 9 +- 5 files changed, 102 insertions(+), 123 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 35d388432..5b65443de 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -33,7 +33,8 @@ class Arguments(object): self.parser = argparse.ArgumentParser(description=description) def _load_args(self) -> None: - self.common_args_parser() + self.common_options() + self.main_options() self._build_subcommands() def get_parsed_arg(self) -> argparse.Namespace: @@ -60,62 +61,65 @@ class Arguments(object): return parsed_arg - def common_args_parser(self) -> None: + def common_options(self) -> None: """ - Parses given common arguments and returns them as a parsed object. + Parses arguments that are common for the main Freqtrade, all subcommands and scripts. """ - self.parser.add_argument( + parser = self.parser + + parser.add_argument( '-v', '--verbose', help='Verbose mode (-vv for more, -vvv to get all messages).', action='count', dest='loglevel', default=0, ) - self.parser.add_argument( + parser.add_argument( '--logfile', help='Log to the file specified', dest='logfile', - type=str, - metavar='FILE' + metavar='FILE', ) - self.parser.add_argument( + parser.add_argument( '--version', action='version', version=f'%(prog)s {__version__}' ) - self.parser.add_argument( + parser.add_argument( '-c', '--config', help='Specify configuration file (default: %(default)s). ' 'Multiple --config options may be used.', dest='config', action='append', - type=str, metavar='PATH', ) - self.parser.add_argument( + parser.add_argument( '-d', '--datadir', help='Path to backtest data.', dest='datadir', - default=None, - type=str, metavar='PATH', ) - self.parser.add_argument( + + def main_options(self) -> None: + """ + Parses arguments for the main Freqtrade. + """ + parser = self.parser + + parser.add_argument( '-s', '--strategy', help='Specify strategy class name (default: %(default)s).', dest='strategy', default='DefaultStrategy', - type=str, metavar='NAME', ) - self.parser.add_argument( + parser.add_argument( '--strategy-path', help='Specify additional strategy lookup path.', dest='strategy_path', - type=str, metavar='PATH', ) - self.parser.add_argument( + parser.add_argument( '--dynamic-whitelist', help='Dynamically generate and update whitelist' ' based on 24h BaseVolume (default: %(const)s).' @@ -126,52 +130,46 @@ class Arguments(object): metavar='INT', nargs='?', ) - self.parser.add_argument( + parser.add_argument( '--db-url', help='Override trades database URL, this is useful if dry_run is enabled' ' or in custom deployments (default: %(default)s).', dest='db_url', - type=str, metavar='PATH', ) - self.parser.add_argument( + parser.add_argument( '--sd-notify', help='Notify systemd service manager.', action='store_true', dest='sd_notify', ) - @staticmethod - def optimizer_shared_options(parser: argparse.ArgumentParser) -> None: + def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None: """ - Parses given common arguments for Backtesting, Edge and Hyperopt modules. + Parses arguments common for Backtesting, Edge and Hyperopt modules. :param parser: - :return: """ + parser = subparser or self.parser + parser.add_argument( '-i', '--ticker-interval', help='Specify ticker interval (1m, 5m, 30m, 1h, 1d).', dest='ticker_interval', - type=str, ) parser.add_argument( '--timerange', help='Specify what timerange of data to use.', - default=None, - type=str, dest='timerange', ) parser.add_argument( '--max_open_trades', help='Specify max_open_trades to use.', - default=None, type=int, dest='max_open_trades', ) parser.add_argument( '--stake_amount', help='Specify stake_amount.', - default=None, type=float, dest='stake_amount', ) @@ -184,11 +182,12 @@ class Arguments(object): dest='refresh_pairs', ) - @staticmethod - def backtesting_options(parser: argparse.ArgumentParser) -> None: + def backtesting_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses given arguments for Backtesting module. """ + parser = subparser or self.parser + parser.add_argument( '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', @@ -224,8 +223,6 @@ class Arguments(object): '--export', help='Export backtest results, argument are: trades. ' 'Example --export=trades', - type=str, - default=None, dest='export', ) parser.add_argument( @@ -234,37 +231,36 @@ class Arguments(object): requires --export to be set as well\ Example --export-filename=user_data/backtest_data/backtest_today.json\ (default: %(default)s)', - type=str, default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), dest='exportfilename', metavar='PATH', ) - @staticmethod - def edge_options(parser: argparse.ArgumentParser) -> None: + def edge_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses given arguments for Edge module. """ + parser = subparser or self.parser + parser.add_argument( '--stoplosses', help='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', - type=str, dest='stoploss_range', ) - @staticmethod - def hyperopt_options(parser: argparse.ArgumentParser) -> None: + def hyperopt_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses given arguments for Hyperopt module. """ + parser = subparser or self.parser + parser.add_argument( '--customhyperopt', help='Specify hyperopt class name (default: %(default)s).', dest='hyperopt', default=constants.DEFAULT_HYPEROPT, - type=str, metavar='NAME', ) parser.add_argument( @@ -321,7 +317,6 @@ class Arguments(object): '--random-state', help='Set random state to some positive integer for reproducible hyperopt results.', dest='hyperopt_random_state', - default=None, type=Arguments.check_int_positive, metavar='INT', ) @@ -335,11 +330,12 @@ class Arguments(object): metavar='INT', ) - @staticmethod - def list_exchanges_options(parser: argparse.ArgumentParser) -> None: + def list_exchanges_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses given arguments for the list-exchanges command. """ + parser = subparser or self.parser + parser.add_argument( '-1', '--one-column', help='Print exchanges in one column', @@ -349,7 +345,7 @@ class Arguments(object): def _build_subcommands(self) -> None: """ - Builds and attaches all subcommands + Builds and attaches all subcommands. :return: None """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge @@ -360,19 +356,19 @@ class Arguments(object): # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd.set_defaults(func=start_backtesting) - self.optimizer_shared_options(backtesting_cmd) + self.common_optimize_options(backtesting_cmd) self.backtesting_options(backtesting_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd.set_defaults(func=start_edge) - self.optimizer_shared_options(edge_cmd) + self.common_optimize_options(edge_cmd) self.edge_options(edge_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd.set_defaults(func=start_hyperopt) - self.optimizer_shared_options(hyperopt_cmd) + self.common_optimize_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd) # Add list-exchanges subcommand @@ -437,69 +433,43 @@ class Arguments(object): ) return uint - def scripts_options(self) -> None: + def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None: """ - Parses given arguments for scripts. + Parses arguments common for scripts. """ - self.parser.add_argument( + parser = subparser or self.parser + + parser.add_argument( '-p', '--pairs', help='Show profits for only this pairs. Pairs are comma-separated.', dest='pairs', - default=None ) def download_data_options(self) -> None: """ - Parses given arguments for testdata download + Parses given arguments for testdata download script """ - self.parser.add_argument( - '-v', '--verbose', - help='Verbose mode (-vv for more, -vvv to get all messages).', - action='count', - dest='loglevel', - default=0, - ) - self.parser.add_argument( - '--logfile', - help='Log to the file specified', - dest='logfile', - type=str, - metavar='FILE', - ) - self.parser.add_argument( - '-c', '--config', - help='Specify configuration file (default: %(default)s). ' - 'Multiple --config options may be used.', - dest='config', - action='append', - type=str, - metavar='PATH', - ) - self.parser.add_argument( - '-d', '--datadir', - help='Path to backtest data.', - dest='datadir', - metavar='PATH', - ) - self.parser.add_argument( + parser = self.parser + + parser.add_argument( '--pairs-file', help='File containing a list of pairs to download.', dest='pairs_file', metavar='FILE', ) - self.parser.add_argument( + parser.add_argument( '--days', help='Download data for given number of days.', dest='days', type=Arguments.check_int_positive, metavar='INT', ) - self.parser.add_argument( + parser.add_argument( '--exchange', help='Exchange name (default: %(default)s). Only valid if no config is provided.', dest='exchange', ) - self.parser.add_argument( + parser.add_argument( '-t', '--timeframes', help='Specify which tickers to download. Space separated list. \ Default: %(default)s.', @@ -508,9 +478,39 @@ class Arguments(object): nargs='+', dest='timeframes', ) - self.parser.add_argument( + parser.add_argument( '--erase', help='Clean all existing data for the selected exchange/pairs/timeframes.', dest='erase', action='store_true' ) + + def plot_dataframe_options(self) -> None: + """ + Parses given arguments for plot dataframe script + """ + parser = self.parser + + parser.add_argument( + '--indicators1', + help='Set indicators from your strategy you want in the first row of the graph. Separate ' + 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', + default='sma,ema3,ema5', + dest='indicators1', + ) + + parser.add_argument( + '--indicators2', + help='Set indicators from your strategy you want in the third row of the graph. Separate ' + 'them with a coma. E.g: fastd,fastk (default: %(default)s)', + default='macd,macdsignal', + dest='indicators2', + ) + parser.add_argument( + '--plot-limit', + help='Specify tick limit for plotting - too high values cause huge files - ' + 'Default: %(default)s', + dest='plot_limit', + default=750, + type=int, + ) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index afa42f287..7a5047507 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -47,9 +47,9 @@ def test_parse_args_verbose() -> None: assert args.loglevel == 1 -def test_scripts_options() -> None: +def test_common_scripts_options() -> None: arguments = Arguments(['-p', 'ETH/BTC'], '') - arguments.scripts_options() + arguments.common_scripts_options() args = arguments.get_parsed_arg() assert args.pairs == 'ETH/BTC' diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 6263d0e2f..dd4627c14 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -20,7 +20,8 @@ logger = logging.getLogger('download_backtest_data') DEFAULT_DL_PATH = 'user_data/data' -arguments = Arguments(sys.argv[1:], 'download utility') +arguments = Arguments(sys.argv[1:], 'Download backtest data') +arguments.common_options() arguments.download_data_options() # Do not read the default config if config is not specified diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 4f8ffb32b..19a4d3506 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -349,35 +349,12 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph dataframe') - arguments.scripts_options() - arguments.parser.add_argument( - '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', - type=str, - default='sma,ema3,ema5', - dest='indicators1', - ) - - arguments.parser.add_argument( - '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a coma. E.g: fastd,fastk (default: %(default)s)', - type=str, - default='macd,macdsignal', - dest='indicators2', - ) - arguments.parser.add_argument( - '--plot-limit', - help='Specify tick limit for plotting - too high values cause huge files - ' - 'Default: %(default)s', - dest='plot_limit', - default=750, - type=int, - ) - arguments.common_args_parser() - arguments.optimizer_shared_options(arguments.parser) - arguments.backtesting_options(arguments.parser) + arguments.common_options() + arguments.main_options() + arguments.common_optimize_options() + arguments.backtesting_options() + arguments.common_scripts_options() + arguments.plot_dataframe_options() return arguments.parse_args() diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 5f7d42c87..fd98c120c 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -206,10 +206,11 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph profits') - arguments.scripts_options() - arguments.common_args_parser() - arguments.optimizer_shared_options(arguments.parser) - arguments.backtesting_options(arguments.parser) + arguments.common_options() + arguments.main_options() + arguments.common_optimize_options() + arguments.backtesting_options() + arguments.common_scripts_options() return arguments.parse_args() From c6fed4e4931d1b9e4cbab813a1b1eb48cdc8c108 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 19 Jun 2019 02:42:29 +0300 Subject: [PATCH 75/92] make flake happy --- freqtrade/arguments.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 5b65443de..01182f2df 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -493,16 +493,16 @@ class Arguments(object): parser.add_argument( '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', + help='Set indicators from your strategy you want in the first row of the graph. ' + 'Separate them with a coma. E.g: ema3,ema5 (default: %(default)s)', default='sma,ema3,ema5', dest='indicators1', ) parser.add_argument( '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a coma. E.g: fastd,fastk (default: %(default)s)', + help='Set indicators from your strategy you want in the third row of the graph. ' + 'Separate them with a coma. E.g: fastd,fastk (default: %(default)s)', default='macd,macdsignal', dest='indicators2', ) From 860e05636699bdd890fb4646418da7c1702a06bf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 19 Jun 2019 02:49:12 +0300 Subject: [PATCH 76/92] --datadir is now handled in arguments.common_options() --- freqtrade/tests/test_arguments.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 7a5047507..89c910a28 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -178,6 +178,7 @@ def test_download_data_options() -> None: '--exchange', 'binance' ] arguments = Arguments(args, '') + arguments.common_options() arguments.download_data_options() args = arguments.parse_args() assert args.pairs_file == 'file_with_pairs' From 0866b5f29f37545e85dc642d595a0a96ee045eaf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 20 Jun 2019 00:04:11 +0300 Subject: [PATCH 77/92] allow reading config from stdin --- docs/bot-usage.md | 3 ++- freqtrade/arguments.py | 5 +++-- freqtrade/configuration.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index cb98e1ea5..b215d7b7c 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -26,7 +26,8 @@ optional arguments: --version show program's version number and exit -c PATH, --config PATH Specify configuration file (default: None). Multiple - --config options may be used. + --config options may be used. Can be set to '-' to + read config from stdin. -d PATH, --datadir PATH Path to backtest data. -s NAME, --strategy NAME diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 35d388432..0e92f8845 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -85,8 +85,9 @@ class Arguments(object): ) self.parser.add_argument( '-c', '--config', - help='Specify configuration file (default: %(default)s). ' - 'Multiple --config options may be used.', + help="Specify configuration file (default: %(default)s). " + "Multiple --config options may be used. " + "Can be set to '-' to read config from stdin.", dest='config', action='append', type=str, diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 01bbb955d..47b4bb8f7 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -79,6 +79,7 @@ class Configuration(object): # Now expecting a list of config filenames here, not a string for path in self.args.config: logger.info('Using config: %s ...', path) + # Merge config options, overwriting old values config = deep_merge_dicts(self._load_config_file(path), config) @@ -118,7 +119,8 @@ class Configuration(object): :return: configuration as dictionary """ try: - with open(path) as file: + # Read config from stdin if requested in the options + with open(path) if path != '-' else sys.stdin as file: conf = json.load(file) except FileNotFoundError: raise OperationalException( From 911e71cd9b5c87c0c04930c8324966296ec06ad0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:30:05 +0200 Subject: [PATCH 78/92] remove redundant test-functions --- freqtrade/tests/conftest.py | 12 ++++++++ freqtrade/tests/test_freqtradebot.py | 41 +++------------------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 034cb5f8b..c857d33e3 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -110,11 +110,23 @@ def patch_freqtradebot(mocker, config) -> None: def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: + """ + This function patches _init_modules() to not call dependencies + :param mocker: a Mocker object to apply patches + :param config: Config to pass to the bot + :return: FreqtradeBot + """ patch_freqtradebot(mocker, config) return FreqtradeBot(config) def get_patched_worker(mocker, config) -> Worker: + """ + This function patches _init_modules() to not call dependencies + :param mocker: a Mocker object to apply patches + :param config: Config to pass to the bot + :return: Worker + """ patch_freqtradebot(mocker, config) return Worker(args=None, config=config) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 65225689b..31c4a791a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -19,47 +19,14 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellCheckTuple, SellType -from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, - patch_exchange, patch_get_signal, +from freqtrade.tests.conftest import (get_patched_freqtradebot, + get_patched_worker, log_has, log_has_re, + patch_edge, patch_exchange, + patch_freqtradebot, patch_get_signal, patch_wallet) from freqtrade.worker import Worker -# Functions for recurrent object patching -def patch_freqtradebot(mocker, config) -> None: - """ - This function patches _init_modules() to not call dependencies - :param mocker: a Mocker object to apply patches - :param config: Config to pass to the bot - :return: None - """ - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - patch_exchange(mocker) - - -def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: - """ - This function patches _init_modules() to not call dependencies - :param mocker: a Mocker object to apply patches - :param config: Config to pass to the bot - :return: FreqtradeBot - """ - patch_freqtradebot(mocker, config) - return FreqtradeBot(config) - - -def get_patched_worker(mocker, config) -> Worker: - """ - This function patches _init_modules() to not call dependencies - :param mocker: a Mocker object to apply patches - :param config: Config to pass to the bot - :return: Worker - """ - patch_freqtradebot(mocker, config) - return Worker(args=None, config=config) - - def patch_RPCManager(mocker) -> MagicMock: """ This function mock RPC manager to avoid repeating this code in almost every tests From dd379c4192771dd7d56a80be162862b911906986 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:32:46 +0200 Subject: [PATCH 79/92] Cancelling stoploss order should not kill the bot --- freqtrade/freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 305a97ba6..0c6ebdeff 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -843,7 +843,10 @@ class FreqtradeBot(object): # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: - self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + try: + self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") # Execute sell and update trade record order_id = self.exchange.sell(pair=str(trade.pair), From a8dcfc05c58d7d74a20f79122db4b079f1629e9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:36:39 +0200 Subject: [PATCH 80/92] Add test to verify InvalidOrder is handled correctly --- freqtrade/tests/test_freqtradebot.py | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 31c4a791a..5229eb2c6 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2075,6 +2075,35 @@ 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: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + sellmock = MagicMock() + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets), + sell=sellmock + ) + + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + patch_get_signal(freqtrade) + freqtrade.create_trade() + + trade = Trade.query.first() + Trade.session = MagicMock() + + freqtrade.config['dry_run'] = False + trade.stoploss_order_id = "abcd" + + freqtrade.execute_sell(trade=trade, limit=1234, + sell_reason=SellType.STOP_LOSS) + assert sellmock.call_count == 1 + assert log_has('Could not cancel stoploss order abcd', caplog.record_tuples) + + def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: From 63640518da73fa8722dc70d5b7f73c3db5e69305 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:56:58 +0200 Subject: [PATCH 81/92] Gracefully handle errosr when cancelling stoploss orders fixes #1933 --- freqtrade/freqtradebot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0c6ebdeff..c601465d9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -691,13 +691,22 @@ class FreqtradeBot(object): # cancelling the current stoploss on exchange first logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})' 'in order to add another one ...', order['id']) - if self.exchange.cancel_order(order['id'], trade.pair): + try: + self.exchange.cancel_order(order['id'], trade.pair) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {order['id']} " + f"for pair {trade.pair}") + + try: # creating the new one stoploss_order_id = self.exchange.stoploss_limit( pair=trade.pair, amount=trade.amount, stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99 )['id'] trade.stoploss_order_id = str(stoploss_order_id) + except DependencyException: + logger.exception(f"Could create trailing stoploss order " + f"for pair {trade.pair}.") def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: From 89ba649ddb7e053fae0350e98689d43f9ade004e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:57:15 +0200 Subject: [PATCH 82/92] Test handling errors while trailing stop loss --- freqtrade/tests/test_freqtradebot.py | 77 ++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 5229eb2c6..87b344853 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -22,8 +22,7 @@ from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.tests.conftest import (get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, - patch_freqtradebot, patch_get_signal, - patch_wallet) + patch_get_signal, patch_wallet) from freqtrade.worker import Worker @@ -1143,6 +1142,77 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, stop_price=0.00002344 * 0.95) +def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, + markets, limit_buy_order, + limit_sell_order) -> None: + # When trailing stoploss is set + stoploss_limit = MagicMock(return_value={'id': 13434334}) + patch_exchange(mocker) + + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + markets=PropertyMock(return_value=markets), + stoploss_limit=stoploss_limit + ) + + # enabling TSL + default_conf['trailing_stop'] = True + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.stoploss = -0.05 + + # setting stoploss_on_exchange_interval to 60 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 + patch_get_signal(freqtrade) + freqtrade.create_trade() + trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = "abcd" + trade.stop_loss = 0.2 + trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None) + + stoploss_order_hanging = { + 'id': "abcd", + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'info': { + 'stopPrice': '0.1' + } + } + mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", + caplog.record_tuples) + + # Still try to create order + assert stoploss_limit.call_count == 1 + + # Fail creating stoploss order + caplog.clear() + cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock()) + mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException()) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + assert cancel_mock.call_count == 1 + assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*", + caplog.record_tuples) + + def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, markets, limit_buy_order, limit_sell_order) -> None: @@ -2075,7 +2145,8 @@ 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, + markets, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) sellmock = MagicMock() From f907a487c87c2d3ee1ddc30066d2cfdabacda48d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 07:06:54 +0200 Subject: [PATCH 83/92] make ticker_interval available to hyperopt functions --- docs/hyperopt.md | 7 ++++++- freqtrade/resolvers/hyperopt_resolver.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 79ea4771b..15b02b56f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -12,7 +12,7 @@ and still take a long time. ## Prepare Hyperopting Before we start digging into Hyperopt, we recommend you to take a look at -an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py) +an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/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. @@ -71,6 +71,11 @@ Place the corresponding settings into the following methods The configuration and rules are the same than for buy signals. To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. +#### Using ticker-interval as part of the Strategy + +The Strategy exposes the ticker-interval as `self.ticker_interval`. The same value is available as class-attribute `HyperoptName.ticker_interval`. +In the case of the linked sample-value this would be `SampleHyperOpts.ticker_interval`. + ## Solving a Mystery Let's say you are curious: should you use MACD crossings or lower Bollinger diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index e7683bc78..638148ee2 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -32,6 +32,9 @@ class HyperOptResolver(IResolver): hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) + # Assign ticker_interval to be used in hyperopt + self.hyperopt.__class__.ticker_interval = config.get('ticker_interval') + if not hasattr(self.hyperopt, 'populate_buy_trend'): logger.warning("Custom Hyperopt does not provide populate_buy_trend. " "Using populate_buy_trend from DefaultStrategy.") From 1a27ae8a817632a446d0a8b139d79055ff5120f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 07:07:39 +0200 Subject: [PATCH 84/92] Add tests to verify that ticker_interval is there --- freqtrade/tests/optimize/test_hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index baa5da545..c40baccbc 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -167,6 +167,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: "Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples) assert log_has("Custom Hyperopt does not provide populate_buy_trend. " "Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples) + assert hasattr(x, "ticker_interval") def test_start(mocker, default_conf, caplog) -> None: From 7a0d86660e5d02658c0201203bcd503d14add826 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 07:10:30 +0200 Subject: [PATCH 85/92] Mypy type errors --- freqtrade/optimize/hyperopt_interface.py | 1 + freqtrade/resolvers/hyperopt_resolver.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 622de3015..08823ece0 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -20,6 +20,7 @@ class IHyperOpt(ABC): stoploss -> float: optimal stoploss designed for the strategy ticker_interval -> int: value of the ticker interval to use for the strategy """ + ticker_interval: str @staticmethod @abstractmethod diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 638148ee2..9333bb09a 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -33,7 +33,7 @@ class HyperOptResolver(IResolver): self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) # Assign ticker_interval to be used in hyperopt - self.hyperopt.__class__.ticker_interval = config.get('ticker_interval') + self.hyperopt.__class__.ticker_interval = str(config['ticker_interval']) if not hasattr(self.hyperopt, 'populate_buy_trend'): logger.warning("Custom Hyperopt does not provide populate_buy_trend. " From a581ca66bf33e0e9fa4b3a36eea0b56804fd33df Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 19:31:18 +0200 Subject: [PATCH 86/92] Adapt test after merging develop --- freqtrade/arguments.py | 1 + freqtrade/tests/test_arguments.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 3075dd0fe..59123a14b 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -444,6 +444,7 @@ class Arguments(object): '-p', '--pairs', help='Show profits for only this pairs. Pairs are comma-separated.', dest='pairs', + required=True ) def download_data_options(self) -> None: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 78ca9d055..4c2bf708d 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -195,6 +195,7 @@ def test_plot_dataframe_options() -> None: '-p', 'UNITTEST/BTC', ] arguments = Arguments(args, '') + arguments.common_scripts_options() arguments.plot_dataframe_options() pargs = arguments.parse_args(True) assert pargs.indicators1 == "sma10,sma100" @@ -205,6 +206,7 @@ def test_plot_dataframe_options() -> None: # Pop pairs argument args = args[:-2] arguments = Arguments(args, '') + arguments.common_scripts_options() arguments.plot_dataframe_options() with pytest.raises(SystemExit, match=r'2'): arguments.parse_args(True) From db17b20e26a563048838d98ca5c6cc4f35381fc1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 20:21:03 +0200 Subject: [PATCH 87/92] Don't require pairs but fall back to pair_whitelist instead --- freqtrade/arguments.py | 1 - freqtrade/tests/test_arguments.py | 8 -------- scripts/plot_dataframe.py | 5 ++++- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 59123a14b..3075dd0fe 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -444,7 +444,6 @@ class Arguments(object): '-p', '--pairs', help='Show profits for only this pairs. Pairs are comma-separated.', dest='pairs', - required=True ) def download_data_options(self) -> None: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 4c2bf708d..d9292bdb5 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -203,14 +203,6 @@ def test_plot_dataframe_options() -> None: assert pargs.plot_limit == 30 assert pargs.pairs == "UNITTEST/BTC" - # Pop pairs argument - args = args[:-2] - arguments = Arguments(args, '') - arguments.common_scripts_options() - arguments.plot_dataframe_options() - with pytest.raises(SystemExit, match=r'2'): - arguments.parse_args(True) - def test_check_int_positive() -> None: diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index a17076085..4aacc99dd 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -104,7 +104,10 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): exchange = ExchangeResolver(exchange_name, config).exchange strategy = StrategyResolver(config).strategy - pairs = config["pairs"].split(',') + if "pairs" in config: + pairs = config["pairs"].split(',') + else: + pairs = config["exchange"]["pair_whitelist"] # Set timerange to use timerange = Arguments.parse_timerange(config["timerange"]) From 026784efac660d2a891ef1e1ee99dfa17b861a16 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 16:45:38 +0200 Subject: [PATCH 88/92] remove get_tickers_data from plot_dataframe --- scripts/plot_dataframe.py | 48 +++++++++------------------------------ 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 4aacc99dd..23e70a987 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -31,7 +31,7 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import Arguments from freqtrade.data import history from freqtrade.data.btanalysis import load_trades, extract_trades_of_period from freqtrade.optimize import setup_configuration @@ -43,38 +43,6 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, - datadir: Path, refresh_pairs: bool, live: bool): - """ - Get tickers data for each pairs on live or local, option defined in args - :return: dictionary of tickers. output format: {'pair': tickersdata} - """ - - ticker_interval = strategy.ticker_interval - - tickers = history.load_data( - datadir=datadir, - pairs=pairs, - ticker_interval=ticker_interval, - refresh_pairs=refresh_pairs, - timerange=timerange, - exchange=exchange, - live=live, - ) - - # No ticker found, impossible to download, len mismatch - for pair, data in tickers.copy().items(): - logger.debug("checking tickers data of pair: %s", pair) - logger.debug("data.empty: %s", data.empty) - logger.debug("len(data): %s", len(data)) - if data.empty: - del tickers[pair] - logger.info( - 'An issue occured while retreiving data of %s pair, please retry ' - 'using -l option for live or --refresh-pairs-cached', pair) - return tickers - - def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: """ Get tickers then Populate strategy indicators and signals, then return the full dataframe @@ -113,10 +81,16 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): timerange = Arguments.parse_timerange(config["timerange"]) ticker_interval = strategy.ticker_interval - tickers = get_tickers_data(strategy, exchange, pairs, timerange, - datadir=Path(str(config.get("datadir"))), - refresh_pairs=config.get('refresh_pairs', False), - live=config.get("live", False)) + tickers = history.load_data( + datadir=Path(str(config.get("datadir"))), + pairs=pairs, + ticker_interval=config['ticker_interval'], + refresh_pairs=config.get('refresh_pairs', False), + timerange=timerange, + exchange=exchange, + live=config.get("live", False), + ) + pair_counter = 0 for pair, data in tickers.items(): pair_counter += 1 From 4cbcb5f36f365bab31d6a16e12f2ceeb885201d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 16:52:14 +0200 Subject: [PATCH 89/92] Move .title to ExchangeResolver (it does not make sense to do this over and over again) --- freqtrade/freqtradebot.py | 3 +-- freqtrade/optimize/backtesting.py | 3 +-- freqtrade/resolvers/exchange_resolver.py | 1 + freqtrade/tests/conftest.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 4 ++-- scripts/plot_dataframe.py | 3 +-- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 305a97ba6..480bb680f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -53,8 +53,7 @@ class FreqtradeBot(object): self.rpc: RPCManager = RPCManager(self) - exchange_name = self.config.get('exchange', {}).get('name').title() - self.exchange = ExchangeResolver(exchange_name, self.config).exchange + self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.wallets = Wallets(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6cc78ad2b..8bdf66f92 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -63,8 +63,7 @@ class Backtesting(object): self.config['dry_run'] = True self.strategylist: List[IStrategy] = [] - exchange_name = self.config.get('exchange', {}).get('name').title() - self.exchange = ExchangeResolver(exchange_name, self.config).exchange + self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.fee = self.exchange.get_fee() if self.config.get('runmode') != RunMode.HYPEROPT: diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 8d1845c71..25a86dd0e 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -22,6 +22,7 @@ class ExchangeResolver(IResolver): Load the custom class from config parameter :param config: configuration dictionary """ + exchange_name = exchange_name.title() try: self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) except ImportError: diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index e956d89c4..3523b44c4 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -60,7 +60,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchang patch_exchange(mocker, api_mock, id) config["exchange"]["name"] = id try: - exchange = ExchangeResolver(id.title(), config).exchange + exchange = ExchangeResolver(id, config).exchange except ImportError: exchange = Exchange(config) return exchange diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index f0dc96626..48a8538a9 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -124,14 +124,14 @@ def test_exchange_resolver(default_conf, mocker, caplog): caplog.record_tuples) caplog.clear() - exchange = ExchangeResolver('Kraken', default_conf).exchange + exchange = ExchangeResolver('kraken', default_conf).exchange assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) assert not isinstance(exchange, Binance) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog.record_tuples) - exchange = ExchangeResolver('Binance', default_conf).exchange + exchange = ExchangeResolver('binance', default_conf).exchange assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 23e70a987..3792233de 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -68,8 +68,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): -Generate plot files :return: None """ - exchange_name = config.get('exchange', {}).get('name').title() - exchange = ExchangeResolver(exchange_name, config).exchange + exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange strategy = StrategyResolver(config).strategy if "pairs" in config: From 451d4a400e3b92236cb40eee10463691b7eff77e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 22 Jun 2019 23:51:29 +0300 Subject: [PATCH 90/92] fix help strings shown to the user --- freqtrade/arguments.py | 39 ++++++++++++++++++++------------------- freqtrade/constants.py | 2 ++ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 3075dd0fe..aa1a3e6da 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -87,9 +87,9 @@ class Arguments(object): ) parser.add_argument( '-c', '--config', - help="Specify configuration file (default: %(default)s). " - "Multiple --config options may be used. " - "Can be set to '-' to read config from stdin.", + help=f'Specify configuration file (default: {constants.DEFAULT_CONFIG}). ' + f'Multiple --config options may be used. ' + f'Can be set to `-` to read config from stdin.', dest='config', action='append', metavar='PATH', @@ -122,9 +122,9 @@ class Arguments(object): ) parser.add_argument( '--dynamic-whitelist', - help='Dynamically generate and update whitelist' - ' based on 24h BaseVolume (default: %(const)s).' - ' DEPRECATED.', + help='Dynamically generate and update whitelist ' + 'based on 24h BaseVolume (default: %(const)s). ' + 'DEPRECATED.', dest='dynamic_whitelist', const=constants.DYNAMIC_WHITELIST, type=int, @@ -133,8 +133,8 @@ class Arguments(object): ) parser.add_argument( '--db-url', - help='Override trades database URL, this is useful if dry_run is enabled' - ' or in custom deployments (default: %(default)s).', + help=f'Override trades database URL, this is useful if dry_run is enabled ' + f'or in custom deployments (default: {constants.DEFAULT_DB_DRYRUN_URL}.', dest='db_url', metavar='PATH', ) @@ -228,10 +228,10 @@ class Arguments(object): ) parser.add_argument( '--export-filename', - help='Save backtest results to this filename \ - requires --export to be set as well\ - Example --export-filename=user_data/backtest_data/backtest_today.json\ - (default: %(default)s)', + help='Save backtest results to this filename ' + 'requires --export to be set as well. ' + 'Example --export-filename=user_data/backtest_data/backtest_today.json ' + '(default: %(default)s)', default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), dest='exportfilename', metavar='PATH', @@ -246,8 +246,8 @@ class Arguments(object): parser.add_argument( '--stoplosses', help='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', + 'the format is "min,max,step" (without any space). ' + 'Example: --stoplosses=-0.01,-0.1,-0.001', dest='stoploss_range', ) @@ -289,8 +289,8 @@ class Arguments(object): ) parser.add_argument( '-s', '--spaces', - help='Specify which parameters to hyperopt. Space separate list. \ - Default: %(default)s.', + help='Specify which parameters to hyperopt. Space separate list. ' + 'Default: %(default)s.', choices=['all', 'buy', 'sell', 'roi', 'stoploss'], default='all', nargs='+', @@ -467,13 +467,14 @@ class Arguments(object): ) parser.add_argument( '--exchange', - help='Exchange name (default: %(default)s). Only valid if no config is provided.', + help=f'Exchange name (default: {constants.DEFAULT_EXCHANGE}). ' + f'Only valid if no config is provided.', dest='exchange', ) parser.add_argument( '-t', '--timeframes', - help='Specify which tickers to download. Space separated list. \ - Default: %(default)s.', + help=f'Specify which tickers to download. Space separated list. ' + f'Default: {constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], nargs='+', diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 4772952fc..7a487fcc7 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -4,6 +4,7 @@ bot constants """ DEFAULT_CONFIG = 'config.json' +DEFAULT_EXCHANGE = 'bittrex' DYNAMIC_WHITELIST = 20 # pairs PROCESS_THROTTLE_SECS = 5 # sec DEFAULT_TICKER_INTERVAL = 5 # min @@ -21,6 +22,7 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 +DEFAULT_DOWNLOAD_TICKER_INTERVALS = '1m 5m' TICKER_INTERVALS = [ '1m', '3m', '5m', '15m', '30m', From 5b84cb39ac0ec5589cef99ccc8cea4b66edbaa54 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sun, 23 Jun 2019 22:51:33 +0300 Subject: [PATCH 91/92] typo fixed --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index d215dc8d6..f0c536ade 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,7 @@ This page explains how to prepare your environment for running the bot. Before running your bot in production you will need to setup few external API. In production mode, the bot will require valid Exchange API -credentials. We also reccomend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). +credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). - [Setup your exchange account](#setup-your-exchange-account) From 116d8e853e40783a6f23a5ba576fc5f433d5a0ad Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 23 Jun 2019 23:10:37 +0300 Subject: [PATCH 92/92] typos in docstrings fixed --- freqtrade/data/btanalysis.py | 4 ++-- freqtrade/data/history.py | 2 +- freqtrade/strategy/interface.py | 4 ++-- scripts/rest_client.py | 32 ++++++++++++++++---------------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 4aecf8ecf..f78ca3fa8 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -23,7 +23,7 @@ def load_backtest_data(filename) -> pd.DataFrame: """ Load backtest data file. :param filename: pathlib.Path object, or string pointing to the file. - :return a dataframe with the analysis results + :return: a dataframe with the analysis results """ if isinstance(filename, str): filename = Path(filename) @@ -78,7 +78,7 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: Load trades, either from a DB (using dburl) or via a backtest export file. :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) :param exportfilename: Path to a file exported from backtesting - :returns: Dataframe containing Trades + :return: Dataframe containing Trades """ timeZone = pytz.UTC diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 67f942119..e9694b90f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -63,7 +63,7 @@ def load_tickerdata_file( timerange: Optional[TimeRange] = None) -> Optional[list]: """ Load a pair from file, either .json.gz or .json - :return tickerlist or None if unsuccesful + :return: tickerlist or None if unsuccesful """ filename = pair_data_filename(datadir, pair, ticker_interval) pairdata = misc.file_load_json(filename) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 68e0a7b37..949a88b91 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -158,7 +158,7 @@ class IStrategy(ABC): """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it - :return DataFrame with ticker data and indicator data + :return: DataFrame with ticker data and indicator data """ pair = str(metadata.get('pair')) @@ -351,7 +351,7 @@ class IStrategy(ABC): """ Based an earlier trade and current price and ROI configuration, decides whether bot should sell. Requires current_profit to be in percent!! - :return True if bot should sell at current rate + :return: True if bot should sell at current rate """ # Check if time matches and current rate is above threshold diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 2261fba0b..a46b3ebfb 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -65,14 +65,14 @@ class FtRestClient(): def start(self): """ Start the bot if it's in stopped state. - :returns: json object + :return: json object """ return self._post("start") def stop(self): """ Stop the bot. Use start to restart - :returns: json object + :return: json object """ return self._post("stop") @@ -80,77 +80,77 @@ class FtRestClient(): """ Stop buying (but handle sells gracefully). use reload_conf to reset - :returns: json object + :return: json object """ return self._post("stopbuy") def reload_conf(self): """ Reload configuration - :returns: json object + :return: json object """ return self._post("reload_conf") def balance(self): """ Get the account balance - :returns: json object + :return: json object """ return self._get("balance") def count(self): """ Returns the amount of open trades - :returns: json object + :return: json object """ return self._get("count") def daily(self, days=None): """ Returns the amount of open trades - :returns: json object + :return: json object """ return self._get("daily", params={"timescale": days} if days else None) def edge(self): """ Returns information about edge - :returns: json object + :return: json object """ return self._get("edge") def profit(self): """ Returns the profit summary - :returns: json object + :return: json object """ return self._get("profit") def performance(self): """ Returns the performance of the different coins - :returns: json object + :return: json object """ return self._get("performance") def status(self): """ Get the status of open trades - :returns: json object + :return: json object """ return self._get("status") def version(self): """ Returns the version of the bot - :returns: json object containing the version + :return: json object containing the version """ return self._get("version") def whitelist(self): """ Show the current whitelist - :returns: json object + :return: json object """ return self._get("whitelist") @@ -158,7 +158,7 @@ class FtRestClient(): """ Show the current blacklist :param add: List of coins to add (example: "BNB/BTC") - :returns: json object + :return: json object """ if not args: return self._get("blacklist") @@ -170,7 +170,7 @@ class FtRestClient(): Buy an asset :param pair: Pair to buy (ETH/BTC) :param price: Optional - price to buy - :returns: json object of the trade + :return: json object of the trade """ data = {"pair": pair, "price": price @@ -181,7 +181,7 @@ class FtRestClient(): """ Force-sell a trade :param tradeid: Id of the trade (can be received via status command) - :returns: json object + :return: json object """ return self._post("forcesell", data={"tradeid": tradeid})