From 383fb6d20ec7934f518df67cf2edcbaaa7e45786 Mon Sep 17 00:00:00 2001 From: Gerald Lonlas Date: Sun, 11 Feb 2018 22:10:21 -0800 Subject: [PATCH] Add a class Arguments to manage cli arguments passed to the bot --- freqtrade/arguments.py | 247 ++++++++++++++++++++++++++++++ freqtrade/tests/test_arguments.py | 127 +++++++++++++++ 2 files changed, 374 insertions(+) create mode 100644 freqtrade/arguments.py create mode 100644 freqtrade/tests/test_arguments.py diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py new file mode 100644 index 000000000..0fa412c85 --- /dev/null +++ b/freqtrade/arguments.py @@ -0,0 +1,247 @@ +""" +This module contains the argument manager class +""" + +import argparse +import os +import re +import logging +from typing import List + +from freqtrade import __version__ +from freqtrade.constants import Constants + + +class Arguments(object): + """ + Arguments Class. Manage the arguments received by the cli + """ + + def __init__(self, args: List[str], description: str): + self.args = args + self.parsed_arg = None + self.parser = argparse.ArgumentParser(description=description) + self._common_args_parser() + self._build_subcommands() + + def get_parsed_arg(self) -> List[str]: + """ + Return the list of arguments + :return: List[str] List of arguments + """ + self.parsed_arg = self._parse_args() + + return self.parsed_arg + + def _parse_args(self) -> List[str]: + """ + Parses given arguments and returns an argparse Namespace instance. + """ + parsed_arg = self.parser.parse_args(self.args) + + return parsed_arg + + def _common_args_parser(self) -> None: + """ + Parses given common arguments and returns them as a parsed object. + """ + self.parser.add_argument( + '-v', '--verbose', + help='be verbose', + action='store_const', + dest='loglevel', + const=logging.DEBUG, + default=logging.INFO, + ) + self.parser.add_argument( + '--version', + action='version', + version='%(prog)s {}'.format(__version__), + ) + self.parser.add_argument( + '-c', '--config', + help='specify configuration file (default: config.json)', + dest='config', + default='config.json', + type=str, + metavar='PATH', + ) + self.parser.add_argument( + '--datadir', + help='path to backtest data (default freqdata/tests/testdata)', + dest='datadir', + default=os.path.join('freqtrade', 'tests', 'testdata'), + type=str, + metavar='PATH', + ) + self.parser.add_argument( + '-s', '--strategy', + help='specify strategy file (default: freqtrade/strategy/default_strategy.py)', + dest='strategy', + default='default_strategy', + type=str, + metavar='PATH', + ) + self.parser.add_argument( + '--dynamic-whitelist', + help='dynamically generate and update whitelist \ + based on 24h BaseVolume (Default 20 currencies)', # noqa + dest='dynamic_whitelist', + const=Constants.DYNAMIC_WHITELIST, + type=int, + metavar='INT', + nargs='?', + ) + self.parser.add_argument( + '--dry-run-db', + help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \ + instead of memory DB. Work only if dry_run is enabled.', + action='store_true', + dest='dry_run_db', + ) + + @staticmethod + def _backtesting_options(parser: argparse.ArgumentParser) -> None: + """ + Parses given arguments for Backtesting scripts. + """ + parser.add_argument( + '-l', '--live', + action='store_true', + dest='live', + help='using live data', + ) + parser.add_argument( + '-i', '--ticker-interval', + help='specify ticker interval in minutes (1, 5, 30, 60, 1440)', + dest='ticker_interval', + type=int, + metavar='INT', + ) + parser.add_argument( + '--realistic-simulation', + help='uses max_open_trades from config to simulate real world limitations', + action='store_true', + dest='realistic_simulation', + ) + parser.add_argument( + '-r', '--refresh-pairs-cached', + help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \ + Use it if you want to run your backtesting with up-to-date data.', + action='store_true', + dest='refresh_pairs', + ) + parser.add_argument( + '--export', + help='Export backtest results, argument are: trades\ + Example --export=trades', + type=str, + default=None, + dest='export', + ) + parser.add_argument( + '--timerange', + help='Specify what timerange of data to use.', + default=None, + type=str, + dest='timerange', + ) + + @staticmethod + def _hyperopt_options(parser: argparse.ArgumentParser) -> None: + """ + Parses given arguments for Hyperopt scripts. + """ + parser.add_argument( + '-e', '--epochs', + help='specify number of epochs (default: 100)', + dest='epochs', + default=Constants.HYPEROPT_EPOCH, + type=int, + metavar='INT', + ) + parser.add_argument( + '--use-mongodb', + help='parallelize evaluations with mongodb (requires mongod in PATH)', + dest='mongodb', + action='store_true', + ) + parser.add_argument( + '-i', '--ticker-interval', + help='specify ticker interval in minutes (1, 5, 30, 60, 1440)', + dest='ticker_interval', + type=int, + metavar='INT', + ) + parser.add_argument( + '--timerange', + help='Specify what timerange of data to use.', + default=None, + type=str, + dest='timerange', + ) + + def _build_subcommands(self) -> None: + """ + Builds and attaches all subcommands + :return: None + """ + from freqtrade.optimize import backtesting, hyperopt + + subparsers = self.parser.add_subparsers(dest='subparser') + + # Add backtesting subcommand + backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module') + backtesting_cmd.set_defaults(func=backtesting.start) + self._backtesting_options(backtesting_cmd) + + # Add hyperopt subcommand + hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') + hyperopt_cmd.set_defaults(func=hyperopt.start) + self._hyperopt_options(hyperopt_cmd) + + @staticmethod + def parse_timerange(text: str) -> (List, int, int): + """ + Parse the value of the argument --timerange to determine what is the range desired + :param text: value from --timerange + :return: Start and End range period + """ + if text is None: + return None + syntax = [(r'^-(\d{8})$', (None, 'date')), + (r'^(\d{8})-$', ('date', None)), + (r'^(\d{8})-(\d{8})$', ('date', 'date')), + (r'^(-\d+)$', (None, 'line')), + (r'^(\d+)-$', ('line', None)), + (r'^(\d+)-(\d+)$', ('index', 'index'))] + for rex, stype in syntax: + # Apply the regular expression to text + match = re.match(rex, text) + if match: # Regex has matched + rvals = match.groups() + index = 0 + start = None + stop = None + if stype[0]: + start = rvals[index] + if stype[0] != 'date': + start = int(start) + index += 1 + if stype[1]: + stop = rvals[index] + if stype[1] != 'date': + stop = int(stop) + return (stype, start, stop) + raise Exception('Incorrect syntax for timerange "%s"' % text) + + def scripts_options(self): + """ + Parses given arguments for plot scripts. + """ + self.parser.add_argument( + '-p', '--pair', + help='Show profits for only this pairs. Pairs are comma-separated.', + dest='pair', + default=None + ) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py new file mode 100644 index 000000000..08125cfdc --- /dev/null +++ b/freqtrade/tests/test_arguments.py @@ -0,0 +1,127 @@ +# pragma pylint: disable=missing-docstring, C0103 + +""" +Unit test file for arguments.py +""" + +import argparse +import logging +import pytest + +from freqtrade.arguments import Arguments + + +def test_arguments_object() -> None: + """ + Test the Arguments object has the mandatory methods + :return: None + """ + assert hasattr(Arguments, 'get_parsed_arg') + assert hasattr(Arguments, '_parse_args') + assert hasattr(Arguments, 'parse_timerange') + assert hasattr(Arguments, 'scripts_options') + + +# Parse common command-line-arguments. Used for all tools +def test_parse_args_none() -> None: + arguments = Arguments([], '') + assert isinstance(arguments, Arguments) + assert isinstance(arguments.parser, argparse.ArgumentParser) + assert isinstance(arguments.parser, argparse.ArgumentParser) + + +def test_parse_args_defaults() -> None: + args = Arguments([], '').get_parsed_arg() + assert args.config == 'config.json' + assert args.dynamic_whitelist is None + assert args.loglevel == logging.INFO + + +def test_parse_args_config() -> None: + args = Arguments(['-c', '/dev/null'], '').get_parsed_arg() + assert args.config == '/dev/null' + + args = Arguments(['--config', '/dev/null'], '').get_parsed_arg() + assert args.config == '/dev/null' + + +def test_parse_args_verbose() -> None: + args = Arguments(['-v'], '').get_parsed_arg() + assert args.loglevel == logging.DEBUG + + args = Arguments(['--verbose'], '').get_parsed_arg() + assert args.loglevel == logging.DEBUG + + +def test_scripts_options() -> None: + arguments = Arguments(['-p', 'BTC_ETH'], '') + arguments.scripts_options() + args = arguments.get_parsed_arg() + assert args.pair == 'BTC_ETH' + + +def test_parse_args_version() -> None: + with pytest.raises(SystemExit, match=r'0'): + Arguments(['--version'], '').get_parsed_arg() + + +def test_parse_args_invalid() -> None: + with pytest.raises(SystemExit, match=r'2'): + Arguments(['-c'], '').get_parsed_arg() + + +def test_parse_args_dynamic_whitelist() -> None: + args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg() + assert args.dynamic_whitelist == 20 + + +def test_parse_args_dynamic_whitelist_10() -> None: + args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg() + assert args.dynamic_whitelist == 10 + + +def test_parse_args_dynamic_whitelist_invalid_values() -> None: + with pytest.raises(SystemExit, match=r'2'): + Arguments(['--dynamic-whitelist', 'abc'], '').get_parsed_arg() + + +def test_parse_timerange_incorrect() -> None: + assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200') + assert (('line', None), 200, None) == Arguments.parse_timerange('200-') + with pytest.raises(Exception, match=r'Incorrect syntax.*'): + Arguments.parse_timerange('-') + + +def test_parse_args_backtesting_invalid() -> None: + with pytest.raises(SystemExit, match=r'2'): + Arguments(['backtesting --ticker-interval'], '').get_parsed_arg() + + with pytest.raises(SystemExit, match=r'2'): + Arguments(['backtesting --ticker-interval', 'abc'], '').get_parsed_arg() + + +def test_parse_args_backtesting_custom() -> None: + args = [ + '-c', 'test_conf.json', + 'backtesting', + '--live', + '--ticker-interval', '1', + '--refresh-pairs-cached'] + call_args = Arguments(args, '').get_parsed_arg() + assert call_args.config == 'test_conf.json' + assert call_args.live is True + assert call_args.loglevel == logging.INFO + assert call_args.subparser == 'backtesting' + assert call_args.func is not None + assert call_args.ticker_interval == 1 + assert call_args.refresh_pairs is True + + +def test_parse_args_hyperopt_custom() -> None: + args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20'] + call_args = Arguments(args, '').get_parsed_arg() + assert call_args.config == 'test_conf.json' + assert call_args.epochs == 20 + assert call_args.loglevel == logging.INFO + assert call_args.subparser == 'hyperopt' + assert call_args.func is not None