diff --git a/freqtrade/main.py b/freqtrade/main.py index e5b7478a4..5e8680b85 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -398,14 +398,18 @@ def cleanup() -> None: exit(0) -def main() -> None: +def main(sysargv=sys.argv[1:]) -> None: """ Loads and validates the config and handles the main loop :return: None """ global _CONF - args = parse_args(sys.argv[1:]) - if not args: + args = parse_args(sysargv, + 'Simple High Frequency Trading Bot for crypto currencies') + + # A subcommand has been issued + if hasattr(args, 'func'): + args.func(args) exit(0) # Initialize logger diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 900aa8763..e62bea8bc 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -81,21 +81,12 @@ def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: return result -def parse_args(args: List[str]): +def parse_args_common(args: List[str], description: str): """ - Parses given arguments and returns an argparse Namespace instance. - Returns None if a sub command has been selected and executed. + Parses given common arguments and returns them as a parsed object. """ parser = argparse.ArgumentParser( - description='Simple High Frequency Trading Bot for crypto currencies' - ) - parser.add_argument( - '-c', '--config', - help='specify configuration file (default: config.json)', - dest='config', - default='config.json', - type=str, - metavar='PATH', + description=description ) parser.add_argument( '-v', '--verbose', @@ -110,6 +101,30 @@ def parse_args(args: List[str]): action='version', version='%(prog)s {}'.format(__version__), ) + parser.add_argument( + '-c', '--config', + help='specify configuration file (default: config.json)', + dest='config', + default='config.json', + type=str, + metavar='PATH', + ) + return parser + + +def parse_args(args: List[str], description: str): + """ + Parses given arguments and returns an argparse Namespace instance. + Returns None if a sub command has been selected and executed. + """ + parser = parse_args_common(args, description) + 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.', # noqa + action='store_true', + dest='dry_run_db', + ) parser.add_argument( '--dynamic-whitelist', help='dynamically generate and update whitelist based on 24h BaseVolume (Default 20 currencies)', # noqa @@ -119,22 +134,9 @@ def parse_args(args: List[str]): metavar='INT', nargs='?', ) - 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.', # noqa - action='store_true', - dest='dry_run_db', - ) + build_subcommands(parser) - parsed_args = parser.parse_args(args) - - # No subcommand as been selected - if not hasattr(parsed_args, 'func'): - return parsed_args - - parsed_args.func(parsed_args) - return None + return parser.parse_args(args) def build_subcommands(parser: argparse.ArgumentParser) -> None: diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index d5ab55cf6..cceb555f7 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -15,6 +15,39 @@ from freqtrade.main import create_trade, handle_trade, init, \ get_target_bid, _process, execute_sell, check_handle_timedout from freqtrade.misc import get_state, State from freqtrade.persistence import Trade +import freqtrade.main as main + + +# Test that main() can start backtesting or hyperopt. +# and also ensure we can pass some specific arguments +# argument parsing is done in test_misc.py + +def test_parse_args_backtesting(mocker): + backtesting_mock = mocker.patch( + 'freqtrade.optimize.backtesting.start', MagicMock()) + with pytest.raises(SystemExit, match=r'0'): + main.main(['backtesting']) + assert backtesting_mock.call_count == 1 + call_args = backtesting_mock.call_args[0][0] + assert call_args.config == 'config.json' + assert call_args.live is False + assert call_args.loglevel == 20 + assert call_args.subparser == 'backtesting' + assert call_args.func is not None + assert call_args.ticker_interval == 5 + + +def test_main_start_hyperopt(mocker): + hyperopt_mock = mocker.patch( + 'freqtrade.optimize.hyperopt.start', MagicMock()) + with pytest.raises(SystemExit, match=r'0'): + main.main(['hyperopt']) + assert hyperopt_mock.call_count == 1 + call_args = hyperopt_mock.call_args[0][0] + assert call_args.config == 'config.json' + assert call_args.loglevel == 20 + assert call_args.subparser == 'hyperopt' + assert call_args.func is not None def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker): diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index cd529039a..0b85000cb 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -1,13 +1,14 @@ # pragma pylint: disable=missing-docstring,C0103 import json import time +import argparse from copy import deepcopy -from unittest.mock import MagicMock import pytest from jsonschema import ValidationError -from freqtrade.misc import throttle, parse_args, load_config +from freqtrade.misc import throttle, parse_args, load_config,\ + parse_args_common def test_throttle(): @@ -38,89 +39,83 @@ def test_throttle_with_assets(): assert result == -1 +# Parse common command-line-arguments +# used for all tools + + +def test_parse_args_none(): + args = parse_args_common([], '') + assert isinstance(args, argparse.ArgumentParser) + + def test_parse_args_defaults(): - args = parse_args([]) - assert args is not None + args = parse_args([], '') assert args.config == 'config.json' assert args.dynamic_whitelist is None assert args.loglevel == 20 -def test_parse_args_invalid(): - with pytest.raises(SystemExit, match=r'2'): - parse_args(['-c']) - - def test_parse_args_config(): - args = parse_args(['-c', '/dev/null']) - assert args is not None + args = parse_args(['-c', '/dev/null'], '') assert args.config == '/dev/null' - args = parse_args(['--config', '/dev/null']) - assert args is not None + args = parse_args(['--config', '/dev/null'], '') assert args.config == '/dev/null' def test_parse_args_verbose(): - args = parse_args(['-v']) - assert args is not None + args = parse_args(['-v'], '') + assert args.loglevel == 10 + + args = parse_args(['--verbose'], '') assert args.loglevel == 10 +def test_parse_args_version(): + with pytest.raises(SystemExit, match=r'0'): + parse_args(['--version'], '') + + +def test_parse_args_invalid(): + with pytest.raises(SystemExit, match=r'2'): + parse_args(['-c'], '') + + +# Parse command-line-arguments +# used for main, backtesting and hyperopt + + def test_parse_args_dynamic_whitelist(): - args = parse_args(['--dynamic-whitelist']) - assert args is not None + args = parse_args(['--dynamic-whitelist'], '') assert args.dynamic_whitelist is 20 def test_parse_args_dynamic_whitelist_10(): - args = parse_args(['--dynamic-whitelist', '10']) - assert args is not None + args = parse_args(['--dynamic-whitelist', '10'], '') assert args.dynamic_whitelist is 10 def test_parse_args_dynamic_whitelist_invalid_values(): with pytest.raises(SystemExit, match=r'2'): - parse_args(['--dynamic-whitelist', 'abc']) - - -def test_parse_args_backtesting(mocker): - backtesting_mock = mocker.patch( - 'freqtrade.optimize.backtesting.start', MagicMock()) - args = parse_args(['backtesting']) - assert args is None - assert backtesting_mock.call_count == 1 - - call_args = backtesting_mock.call_args[0][0] - assert call_args.config == 'config.json' - assert call_args.live is False - assert call_args.loglevel == 20 - assert call_args.subparser == 'backtesting' - assert call_args.func is not None - assert call_args.ticker_interval == 5 + parse_args(['--dynamic-whitelist', 'abc'], '') def test_parse_args_backtesting_invalid(): with pytest.raises(SystemExit, match=r'2'): - parse_args(['backtesting --ticker-interval']) + parse_args(['backtesting --ticker-interval'], '') with pytest.raises(SystemExit, match=r'2'): - parse_args(['backtesting --ticker-interval', 'abc']) + parse_args(['backtesting --ticker-interval', 'abc'], '') -def test_parse_args_backtesting_custom(mocker): - backtesting_mock = mocker.patch( - 'freqtrade.optimize.backtesting.start', MagicMock()) - args = parse_args([ +def test_parse_args_backtesting_custom(): + args = [ '-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1', - '--refresh-pairs-cached']) - assert args is None - assert backtesting_mock.call_count == 1 - - call_args = backtesting_mock.call_args[0][0] + '--refresh-pairs-cached'] + call_args = parse_args(args, '') assert call_args.config == 'test_conf.json' assert call_args.live is True assert call_args.loglevel == 20 @@ -130,28 +125,9 @@ def test_parse_args_backtesting_custom(mocker): assert call_args.refresh_pairs is True -def test_parse_args_hyperopt(mocker): - hyperopt_mock = mocker.patch( - 'freqtrade.optimize.hyperopt.start', MagicMock()) - args = parse_args(['hyperopt']) - assert args is None - assert hyperopt_mock.call_count == 1 - - call_args = hyperopt_mock.call_args[0][0] - assert call_args.config == 'config.json' - assert call_args.loglevel == 20 - assert call_args.subparser == 'hyperopt' - assert call_args.func is not None - - def test_parse_args_hyperopt_custom(mocker): - hyperopt_mock = mocker.patch( - 'freqtrade.optimize.hyperopt.start', MagicMock()) - args = parse_args(['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']) - assert args is None - assert hyperopt_mock.call_count == 1 - - call_args = hyperopt_mock.call_args[0][0] + args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20'] + call_args = parse_args(args, '') assert call_args.config == 'test_conf.json' assert call_args.epochs == 20 assert call_args.loglevel == 20 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 0d193726d..44961891c 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -1,17 +1,33 @@ #!/usr/bin/env python3 +import sys +import argparse import matplotlib # Install PYQT5 manually if you want to test this helper function matplotlib.use("Qt5Agg") import matplotlib.pyplot as plt from freqtrade import exchange, analyze +from freqtrade.misc import parse_args_common -def plot_analyzed_dataframe(pair: str) -> None: +def plot_parse_args(args ): + parser = parse_args_common(args, 'Graph utility') + parser.add_argument( + '-p', '--pair', + help = 'What currency pair', + dest = 'pair', + default = 'BTC_ETH', + type = str, + ) + return parser.parse_args(args) + + +def plot_analyzed_dataframe(args) -> None: """ Calls analyze() and plots the returned dataframe :param pair: pair as str :return: None """ + pair = args.pair # Init Bittrex to use public API exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) @@ -50,4 +66,5 @@ def plot_analyzed_dataframe(pair: str) -> None: if __name__ == '__main__': - plot_analyzed_dataframe('BTC_ETH') + args = plot_parse_args(sys.argv[1:]) + plot_analyzed_dataframe(args)