From a8efb1e1c8f52d290fa7c4cc1c616edfabd72195 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 20 Jun 2019 03:26:02 +0300 Subject: [PATCH 001/191] test for #1948 added --- freqtrade/tests/strategy/test_interface.py | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index e384003dc..6409123e7 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -185,6 +185,39 @@ def test_min_roi_reached2(default_conf, fee) -> None: assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) +def test_min_roi_reached3(default_conf, fee) -> None: + + # test for issue #1948 + min_roi = {20: 0.07, + 30: 0.05, + 55: 0.30, + } + strategy = DefaultStrategy(default_conf) + strategy.minimal_roi = min_roi + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_date=arrow.utcnow().shift(hours=-1).datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, + ) + + assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime) + assert not strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime) + + assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime) + assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime) + + assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime) + assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime) + + # Should not trigger with 20% profit since after 55 minutes only 30% is active. + assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime) + assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) + + def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) From 144e053a4e1349d9037317c3a697e77983484926 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 20 Jun 2019 03:26:25 +0300 Subject: [PATCH 002/191] fix for #1948 --- freqtrade/strategy/interface.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 68e0a7b37..353ae64b4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -357,13 +357,13 @@ class IStrategy(ABC): # Check if time matches and current rate is above threshold trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 - # Get highest entry in ROI dict where key >= trade-duration - roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys()))) + # Get highest entry in ROI dict where key <= trade-duration + roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) + if not roi_list: + return False + roi_entry = max(roi_list) threshold = self.minimal_roi[roi_entry] - if current_profit > threshold: - return True - - return False + return current_profit > threshold def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """ From 7fbdf36c6481f22eef52e75941c898ce8a3f496b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 23 Jun 2019 19:23:51 +0300 Subject: [PATCH 003/191] avoid code duplication while selecting min_roi entries --- freqtrade/optimize/backtesting.py | 17 +++++++--------- freqtrade/strategy/interface.py | 33 ++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6cc78ad2b..e0660eacb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -253,22 +253,19 @@ class Backtesting(object): sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, sell_row.sell, low=sell_row.low, high=sell_row.high) if sell.sell_flag: - trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) # Special handling if high or low hit STOP_LOSS or ROI if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): # Set close_rate to stoploss closerate = trade.stop_loss elif sell.sell_type == (SellType.ROI): - # get next entry in min_roi > to trade duration - # Interface.py skips on trade_duration <= duration - roi_entry = max(list(filter(lambda x: trade_dur >= x, - self.strategy.minimal_roi.keys()))) - roi = self.strategy.minimal_roi[roi_entry] - - # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - closerate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) + roi = self.strategy.min_roi_reached_entry(trade_dur) + if roi is not None: + # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) + closerate = - (trade.open_rate * roi + trade.open_rate * + (1 + trade.fee_open)) / (trade.fee_close - 1) + else: + closerate = sell_row.open else: closerate = sell_row.open diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 353ae64b4..7ec782b0b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,7 +6,7 @@ import logging from abc import ABC, abstractmethod from datetime import datetime from enum import Enum -from typing import Dict, List, NamedTuple, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple import warnings import arrow @@ -347,23 +347,32 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: + def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]: """ - 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 + Based on trade duration defines the ROI entry that may have been reached. + :param trade_dur: trade duration in minutes + :return: minimal ROI entry value or None if none proper ROI entry was found. """ - - # Check if time matches and current rate is above threshold - trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 - # Get highest entry in ROI dict where key <= trade-duration roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) if not roi_list: - return False + return None roi_entry = max(roi_list) - threshold = self.minimal_roi[roi_entry] - return current_profit > threshold + return self.minimal_roi[roi_entry] + + def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: + """ + Based on trade duration, 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 + """ + # Check if time matches and current rate is above threshold + trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) + roi = self.min_roi_reached_entry(trade_dur) + if roi is None: + return False + else: + return current_profit > roi def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """ From c1ee5d69c9b1cb799d8eef7d841c84fb2ee211d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 07:09:54 +0200 Subject: [PATCH 004/191] Try to get travis cache to work correctly --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d5cd52df2..455f4f037 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,4 +56,4 @@ notifications: cache: pip: True directories: - - /usr/local/lib + - /usr/local/lib/ From 1b156e0f344aed2f9025d3460aea400ae8b9a61f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 07:10:24 +0200 Subject: [PATCH 005/191] Don't install python to a system, it's error-prone and may not work --- setup.sh | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/setup.sh b/setup.sh index 11df6820e..8b5531746 100755 --- a/setup.sh +++ b/setup.sh @@ -1,12 +1,21 @@ #!/usr/bin/env bash #encoding=utf8 +function check_installed_pip() { + ${PYTHON} -m pip > /dev/null + if [ $? -ne 0 ]; then + echo "pip not found. Please make sure that pip is available for ${PYTHON}." + exit 1 + fi +} + # Check which python version is installed function check_installed_python() { which python3.7 if [ $? -eq 0 ]; then echo "using Python 3.7" PYTHON=python3.7 + check_installed_pip return fi @@ -14,6 +23,7 @@ function check_installed_python() { if [ $? -eq 0 ]; then echo "using Python 3.6" PYTHON=python3.6 + check_installed_pip return fi @@ -21,7 +31,6 @@ function check_installed_python() { echo "No usable python found. Please make sure to have python3.6 or python3.7 installed" exit 1 fi - } function updateenv() { @@ -29,21 +38,21 @@ function updateenv() { echo "Updating your virtual env" echo "-------------------------" source .env/bin/activate - echo "pip3 install in-progress. Please wait..." + echo "pip install in-progress. Please wait..." # Install numpy first to have py_find_1st install clean - pip3 install --upgrade pip numpy - pip3 install --upgrade -r requirements.txt + ${PYTHON} -m pip install --upgrade pip numpy + ${PYTHON} -m pip install --upgrade -r requirements.txt read -p "Do you want to install dependencies for dev [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then - pip3 install --upgrade -r requirements-dev.txt + ${PYTHON} -m pip install --upgrade -r requirements-dev.txt else echo "Dev dependencies ignored." fi - pip3 install --quiet -e . - echo "pip3 install completed" + ${PYTHON} -m pip install -e . + echo "pip install completed" echo } @@ -74,16 +83,14 @@ function install_macos() { echo "-------------------------" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" fi - brew install python3 wget install_talib test_and_fix_python_on_mac } # Install bot Debian_ubuntu function install_debian() { - sudo add-apt-repository ppa:jonathonf/python-3.6 sudo apt-get update - sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git + sudo apt-get install build-essential autoconf libtool pkg-config make wget git install_talib } @@ -244,7 +251,7 @@ echo " Installing dependencies for Plotting scripts ----------------------------------------- " -pip install plotly --upgrade +${PYTHON} -m pip install plotly --upgrade } function help() { From e1daf0273506ed8204210bf4241a0823b4911624 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 19:46:39 +0200 Subject: [PATCH 006/191] UPdate version for develop --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index a5ae200d4..2fdcccea5 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.6' +__version__ = '2019.6-dev' class DependencyException(Exception): From 7166674d6c4ba0726ce534b5c97358b11092260a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:09:39 +0200 Subject: [PATCH 007/191] Move check_int_positive out of arguments class --- freqtrade/arguments.py | 31 ++++++++++++++++--------------- freqtrade/tests/test_arguments.py | 16 ++++++++-------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 1ec32d1f0..0d0896b2a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -10,6 +10,18 @@ import arrow from freqtrade import __version__, constants +def check_int_positive(value: str) -> int: + try: + uint = int(value) + if uint <= 0: + raise ValueError + except ValueError: + raise argparse.ArgumentTypeError( + f"{value} is invalid for this parameter, should be a positive integer value" + ) + return uint + + class TimeRange(NamedTuple): """ NamedTuple Defining timerange inputs. @@ -33,6 +45,7 @@ class Arguments(object): self.parser = argparse.ArgumentParser(description=description) def _load_args(self) -> None: + # Common options self.common_options() self.main_options() self._build_subcommands() @@ -318,7 +331,7 @@ class Arguments(object): '--random-state', help='Set random state to some positive integer for reproducible hyperopt results.', dest='hyperopt_random_state', - type=Arguments.check_int_positive, + type=check_int_positive, metavar='INT', ) parser.add_argument( @@ -327,7 +340,7 @@ class Arguments(object): "optimization path (default: 1).", dest='hyperopt_min_trades', default=1, - type=Arguments.check_int_positive, + type=check_int_positive, metavar='INT', ) @@ -422,18 +435,6 @@ class Arguments(object): return TimeRange(stype[0], stype[1], start, stop) raise Exception('Incorrect syntax for timerange "%s"' % text) - @staticmethod - def check_int_positive(value: str) -> int: - try: - uint = int(value) - if uint <= 0: - raise ValueError - except ValueError: - raise argparse.ArgumentTypeError( - f"{value} is invalid for this parameter, should be a positive integer value" - ) - return uint - def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses arguments common for scripts. @@ -462,7 +463,7 @@ class Arguments(object): '--days', help='Download data for given number of days.', dest='days', - type=Arguments.check_int_positive, + type=check_int_positive, metavar='INT', ) parser.add_argument( diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index d9292bdb5..20e63d7e7 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,7 +3,7 @@ import argparse import pytest -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import Arguments, TimeRange, check_int_positive # Parse common command-line-arguments. Used for all tools @@ -206,18 +206,18 @@ def test_plot_dataframe_options() -> None: def test_check_int_positive() -> None: - assert Arguments.check_int_positive("3") == 3 - assert Arguments.check_int_positive("1") == 1 - assert Arguments.check_int_positive("100") == 100 + assert check_int_positive("3") == 3 + assert check_int_positive("1") == 1 + assert check_int_positive("100") == 100 with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive("-2") + check_int_positive("-2") with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive("0") + check_int_positive("0") with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive("3.5") + check_int_positive("3.5") with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive("DeadBeef") + check_int_positive("DeadBeef") From 7017e46ba10dc42e81884986d7725ccfb7d50393 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:10:35 +0200 Subject: [PATCH 008/191] Add dict with all possible cli arguments --- freqtrade/arguments.py | 343 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 0d0896b2a..f1624344a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -22,6 +22,342 @@ def check_int_positive(value: str) -> int: return uint +class Arg: + # Optional CLI arguments + def __init__(self, *args, **kwargs): + self.cli = args + self.kwargs = kwargs + + +# List of available command line arguments +AVAILABLE_CLI_OPTIONS = { + # Common arguments + "loglevel": Arg( + '-v', '--verbose', + help='Verbose mode (-vv for more, -vvv to get all messages).', + action='count', + dest='loglevel', + default=0, + ), + "logfile": Arg( + '--logfile', + help='Log to the file specified.', + dest='logfile', + metavar='FILE', + ), + + "version": Arg( + '--version', + action='version', + version=f'%(prog)s {__version__}' + ), + "config": Arg( + '-c', '--config', + 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',), + "datadir": Arg( + '-d', '--datadir', + help='Path to backtest data.', + dest='datadir', + metavar='PATH',), + # Main options + "strategy": Arg( + '-s', '--strategy', + help='Specify strategy class name (default: `%(default)s`).', + dest='strategy', + default='DefaultStrategy', + metavar='NAME', + ), + "strategy_path": Arg( + '--strategy-path', + help='Specify additional strategy lookup path.', + dest='strategy_path', + metavar='PATH', + ), + "dynamic_whitelist": Arg( + '--dynamic-whitelist', + help='Dynamically generate and update whitelist ' + 'based on 24h BaseVolume (default: %(const)s). ' + 'DEPRECATED.', + dest='dynamic_whitelist', + const=constants.DYNAMIC_WHITELIST, + type=int, + metavar='INT', + nargs='?', + ), + "db_url": Arg( + '--db-url', + help=f'Override trades database URL, this is useful in custom deployments ' + f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' + f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', + dest='db_url', + metavar='PATH', + ), + "sd_notify": Arg( + '--sd-notify', + help='Notify systemd service manager.', + action='store_true', + dest='sd_notify', + ), + # Optimize common + "ticker_interval": Arg( + '-i', '--ticker-interval', + help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', + dest='ticker_interval', + ), + "timerange": Arg( + '--timerange', + help='Specify what timerange of data to use.', + dest='timerange', + ), + "max_open_trades": Arg( + '--max_open_trades', + help='Specify max_open_trades to use.', + type=int, + dest='max_open_trades', + ), + "stake_amount": Arg( + '--stake_amount', + help='Specify stake_amount.', + type=float, + dest='stake_amount', + ), + "refresh_pairs": Arg( + '-r', '--refresh-pairs-cached', + help='Refresh the pairs files in tests/testdata with the latest data from the ' + 'exchange. Use it if you want to run your optimization commands with ' + 'up-to-date data.', + action='store_true', + dest='refresh_pairs', + ), + # backtesting + "position_stacking": Arg( + '--eps', '--enable-position-stacking', + help='Allow buying the same pair multiple times (position stacking).', + action='store_true', + dest='position_stacking', + default=False + ), + "use_max_market_positions": Arg( + '--dmmp', '--disable-max-market-positions', + help='Disable applying `max_open_trades` during backtest ' + '(same as setting `max_open_trades` to a very high number).', + action='store_false', + dest='use_max_market_positions', + default=True + ), + "live": Arg( + '-l', '--live', + help='Use live data.', + action='store_true', + dest='live', + ), + "strategy_list": Arg( + '--strategy-list', + help='Provide a comma-separated list of strategies to backtest. ' + 'Please note that ticker-interval needs to be set either in config ' + 'or via command line. When using this together with `--export trades`, ' + 'the strategy-name is injected into the filename ' + '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', + nargs='+', + dest='strategy_list', + ), + "export": Arg( + '--export', + help='Export backtest results, argument are: trades. ' + 'Example: `--export=trades`', + dest='export', + ), + "exportfilename": Arg( + '--export-filename', + help='Save backtest results to the file with this filename (default: `%(default)s`). ' + 'Requires `--export` to be set as well. ' + 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', + default=os.path.join('user_data', 'backtest_data', + 'backtest-result.json'), + dest='exportfilename', + metavar='PATH', + ), + # Edge + "stoploss_range": Arg( + '--stoplosses', + help='Defines a range of stoploss values against which edge will assess the strategy. ' + 'The format is "min,max,step" (without any space). ' + 'Example: `--stoplosses=-0.01,-0.1,-0.001`', + dest='stoploss_range', + ), + # hyperopt + "hyperopt": Arg( + '--customhyperopt', + help='Specify hyperopt class name (default: `%(default)s`).', + dest='hyperopt', + default=constants.DEFAULT_HYPEROPT, + metavar='NAME', + ), + "epochs": Arg( + '-e', '--epochs', + help='Specify number of epochs (default: %(default)d).', + dest='epochs', + default=constants.HYPEROPT_EPOCH, + type=int, + metavar='INT', + ), + "spaces": Arg( + '-s', '--spaces', + help='Specify which parameters to hyperopt. Space-separated list. ' + 'Default: `%(default)s`.', + choices=['all', 'buy', 'sell', 'roi', 'stoploss'], + default='all', + nargs='+', + dest='spaces', + ), + "print_all": Arg( + '--print-all', + help='Print all results, not only the best ones.', + action='store_true', + dest='print_all', + default=False + ), + "hyperopt_jobs": Arg( + '-j', '--job-workers', + help='The number of concurrently running jobs for hyperoptimization ' + '(hyperopt worker processes). ' + 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' + 'If 1 is given, no parallel computing code is used at all.', + dest='hyperopt_jobs', + default=-1, + type=int, + metavar='JOBS', + ), + "hyperopt_random_state": Arg( + '--random-state', + help='Set random state to some positive integer for reproducible hyperopt results.', + dest='hyperopt_random_state', + type=check_int_positive, + metavar='INT', + ), + "hyperopt_min_trades": Arg( + '--min-trades', + help="Set minimal desired number of trades for evaluations in the hyperopt " + "optimization path (default: 1).", + dest='hyperopt_min_trades', + default=1, + type=check_int_positive, + metavar='INT', + ), + # List_exchange + "print_one_column": Arg( + '-1', '--one-column', + help='Print exchanges in one column.', + action='store_true', + dest='print_one_column', + ), + # script_options + "pairs": Arg( + '-p', '--pairs', + help='Show profits for only these pairs. Pairs are comma-separated.', + dest='pairs', + ), + # Download data + + "pairs_file": Arg( + '--pairs-file', + help='File containing a list of pairs to download.', + dest='pairs_file', + metavar='FILE', + ), + "days": Arg( + '--days', + help='Download data for given number of days.', + dest='days', + type=check_int_positive, + metavar='INT', + ), + "exchange": Arg( + '--exchange', + help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' + f'Only valid if no config is provided.', + dest='exchange', + ), + "timeframes": Arg( + '-t', '--timeframes', + 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='+', + dest='timeframes', + ), + "erase": Arg( + '--erase', + help='Clean all existing data for the selected exchange/pairs/timeframes.', + dest='erase', + action='store_true', + ), + # Plot_df_options + "indicators1": Arg( + '--indicators1', + help='Set indicators from your strategy you want in the first row of the graph. ' + 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', + default='sma,ema3,ema5', + dest='indicators1', + ), + "indicators2": Arg( + '--indicators2', + help='Set indicators from your strategy you want in the third row of the graph. ' + 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', + default='macd,macdsignal', + dest='indicators2', + ), + "plot_limit": Arg( + '--plot-limit', + help='Specify tick limit for plotting. Notice: too high values cause huge files. ' + 'Default: %(default)s.', + dest='plot_limit', + default=750, + type=int, + ), + "trade_source": Arg( + '--trade-source', + help='Specify the source for trades (Can be DB or file (backtest file)) ' + 'Default: %(default)s', + dest='trade_source', + default="file", + choices=["DB", "file"] + ) +} + +ARGS_COMMON = ["loglevel", "logfile", "version", "config", "datadir"] +ARGS_STRATEGY = ["strategy", "strategy_path"] + +ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["dynamic_whitelist", "db_url", "sd_notify"] + +ARGS_COMMON_OPTIMIZE = ["loglevel", "ticker_interval", "timerange", + "max_open_trades", "stake_amount", "refresh_pairs"] + +ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", + "live", "strategy_list", "export", "exportfilename"] + +ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", + "use_max_market_positions", "print_all", "hyperopt_jobs", + "hyperopt_random_state", "hyperopt_min_trades"] + +ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] + + +ARGS_LIST_EXCHANGE = ["print_one_column"] + +ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] + +ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", + "export", "exportfilename", "trade_source"]) + + class TimeRange(NamedTuple): """ NamedTuple Defining timerange inputs. @@ -74,6 +410,13 @@ class Arguments(object): return parsed_arg + def build_args(self, optionlist, parser=None): + parser = parser or self.parser + + for val in optionlist: + opt = AVAILABLE_CLI_OPTIONS[val] + parser.add_argument(*opt.cli, **opt.kwargs) + def common_options(self) -> None: """ Parses arguments that are common for the main Freqtrade, all subcommands and scripts. From 7e82be53cde24affda9edee34fe7247d26beb6c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:14:24 +0200 Subject: [PATCH 009/191] Use build_args to build subcomand arguments --- freqtrade/arguments.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f1624344a..8eeb86468 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -381,9 +381,8 @@ class Arguments(object): self.parser = argparse.ArgumentParser(description=description) def _load_args(self) -> None: - # Common options - self.common_options() - self.main_options() + self.build_args(optionlist=ARGS_MAIN) + self._build_subcommands() def get_parsed_arg(self) -> argparse.Namespace: @@ -713,20 +712,17 @@ class Arguments(object): # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd.set_defaults(func=start_backtesting) - self.common_optimize_options(backtesting_cmd) - self.backtesting_options(backtesting_cmd) + self.build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd.set_defaults(func=start_edge) - self.common_optimize_options(edge_cmd) - self.edge_options(edge_cmd) + self.build_args(optionlist=ARGS_EDGE, parser=edge_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd.set_defaults(func=start_hyperopt) - self.common_optimize_options(hyperopt_cmd) - self.hyperopt_options(hyperopt_cmd) + self.build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) # Add list-exchanges subcommand list_exchanges_cmd = subparsers.add_parser( @@ -734,7 +730,7 @@ class Arguments(object): help='Print available exchanges.' ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) - self.list_exchanges_options(list_exchanges_cmd) + self.build_args(optionlist=ARGS_LIST_EXCHANGE, parser=list_exchanges_cmd) @staticmethod def parse_timerange(text: Optional[str]) -> TimeRange: From ee312ac2302a7ffc87cbd2c62c0e16639ae5ec14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:18:26 +0200 Subject: [PATCH 010/191] Use build_args for plot_dataframe script --- freqtrade/arguments.py | 4 ++-- scripts/plot_dataframe.py | 10 +++------- scripts/plot_profit.py | 6 +++--- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 8eeb86468..c5431a665 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -354,8 +354,8 @@ ARGS_LIST_EXCHANGE = ["print_one_column"] ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + - ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", - "export", "exportfilename", "trade_source"]) + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", + "export", "exportfilename", "timerange", "refresh_pairs", "live"]) class TimeRange(NamedTuple): diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7eaf0b337..7e81af925 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 +from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments from freqtrade.data import history from freqtrade.data.btanalysis import (extract_trades_of_period, load_backtest_data, load_trades_from_db) @@ -125,12 +125,8 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph dataframe') - arguments.common_options() - arguments.main_options() - arguments.common_optimize_options() - arguments.backtesting_options() - arguments.common_scripts_options() - arguments.plot_dataframe_options() + arguments.build_args(optionlist=ARGS_PLOT_DATAFRAME) + parsed_args = arguments.parse_args() # Load the configuration diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index fd98c120c..8bc9f2f9a 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -24,7 +24,7 @@ import plotly.graph_objs as go from plotly import tools from plotly.offline import plot -from freqtrade.arguments import Arguments +from freqtrade.arguments import Arguments, ARGS_PLOT_DATAFRAME from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.exchange import timeframe_to_seconds @@ -206,8 +206,8 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph profits') - arguments.common_options() - arguments.main_options() + arguments.build_args(optionlist=ARGS_PLOT_DATAFRAME) + arguments.common_optimize_options() arguments.backtesting_options() arguments.common_scripts_options() From 27798c1683f784f4cc2e1bde8a31ea5bdb7eb465 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:18:54 +0200 Subject: [PATCH 011/191] Remove main_options --- freqtrade/arguments.py | 45 ------------------------------------------ 1 file changed, 45 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index c5431a665..69da90f42 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -456,51 +456,6 @@ class Arguments(object): metavar='PATH', ) - 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', - metavar='NAME', - ) - parser.add_argument( - '--strategy-path', - help='Specify additional strategy lookup path.', - dest='strategy_path', - metavar='PATH', - ) - parser.add_argument( - '--dynamic-whitelist', - help='Dynamically generate and update whitelist ' - 'based on 24h BaseVolume (default: %(const)s). ' - 'DEPRECATED.', - dest='dynamic_whitelist', - const=constants.DYNAMIC_WHITELIST, - type=int, - metavar='INT', - nargs='?', - ) - parser.add_argument( - '--db-url', - help=f'Override trades database URL, this is useful in custom deployments ' - f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' - f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', - dest='db_url', - metavar='PATH', - ) - parser.add_argument( - '--sd-notify', - help='Notify systemd service manager.', - action='store_true', - dest='sd_notify', - ) - def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses arguments common for Backtesting, Edge and Hyperopt modules. From ba7a0dde06ea72666e757a68c90709ab75f03d06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:21:42 +0200 Subject: [PATCH 012/191] Use build_args for download script --- freqtrade/arguments.py | 2 +- freqtrade/tests/test_arguments.py | 6 +++--- scripts/download_backtest_data.py | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 69da90f42..e4c810b23 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -351,7 +351,7 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_LIST_EXCHANGE = ["print_one_column"] -ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] +ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 20e63d7e7..38383a346 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,7 +3,7 @@ import argparse import pytest -from freqtrade.arguments import Arguments, TimeRange, check_int_positive +from freqtrade.arguments import Arguments, TimeRange, check_int_positive, ARGS_DOWNLOADER # Parse common command-line-arguments. Used for all tools @@ -178,8 +178,8 @@ def test_download_data_options() -> None: '--exchange', 'binance' ] arguments = Arguments(args, '') - arguments.common_options() - arguments.download_data_options() + arguments.build_args(ARGS_DOWNLOADER) + args = arguments.parse_args() assert args.pairs_file == 'file_with_pairs' assert args.datadir == 'datadir/folder' diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index dd4627c14..472b3ef4e 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -8,7 +8,7 @@ import sys from pathlib import Path from typing import Any, Dict, List -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import Arguments, TimeRange, ARGS_DOWNLOADER from freqtrade.configuration import Configuration from freqtrade.data.history import download_pair_history from freqtrade.exchange import Exchange @@ -21,8 +21,7 @@ logger = logging.getLogger('download_backtest_data') DEFAULT_DL_PATH = 'user_data/data' arguments = Arguments(sys.argv[1:], 'Download backtest data') -arguments.common_options() -arguments.download_data_options() +arguments.build_args(ARGS_DOWNLOADER) # Do not read the default config if config is not specified # in the command line options explicitely From ca5093901b6e85e224809a00dc05c8967c2ca4ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:27:29 +0200 Subject: [PATCH 013/191] Use build_args for plot script --- freqtrade/arguments.py | 3 +++ scripts/plot_profit.py | 8 ++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index e4c810b23..278f21c97 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -357,6 +357,9 @@ ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", "timerange", "refresh_pairs", "live"]) +ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + + ["pairs", "timerange", "export", "exportfilename"]) + class TimeRange(NamedTuple): """ diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 8bc9f2f9a..32bfae9cc 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -24,7 +24,7 @@ import plotly.graph_objs as go from plotly import tools from plotly.offline import plot -from freqtrade.arguments import Arguments, ARGS_PLOT_DATAFRAME +from freqtrade.arguments import Arguments, ARGS_PLOT_PROFIT from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.exchange import timeframe_to_seconds @@ -206,11 +206,7 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph profits') - arguments.build_args(optionlist=ARGS_PLOT_DATAFRAME) - - arguments.common_optimize_options() - arguments.backtesting_options() - arguments.common_scripts_options() + arguments.build_args(optionlist=ARGS_PLOT_PROFIT) return arguments.parse_args() From b92c6cdf359076c564f5f4e2cfe748d510d4daa1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:32:54 +0200 Subject: [PATCH 014/191] Cleanup arguments and test_arguments --- freqtrade/arguments.py | 334 +----------------------------- freqtrade/tests/test_arguments.py | 10 +- 2 files changed, 8 insertions(+), 336 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 278f21c97..f020252f8 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -354,8 +354,9 @@ ARGS_LIST_EXCHANGE = ["print_one_column"] ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + - ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", - "export", "exportfilename", "timerange", "refresh_pairs", "live"]) + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", + "trade_source", "export", "exportfilename", "timerange", + "refresh_pairs", "live"]) ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "timerange", "export", "exportfilename"]) @@ -419,244 +420,6 @@ class Arguments(object): opt = AVAILABLE_CLI_OPTIONS[val] parser.add_argument(*opt.cli, **opt.kwargs) - def common_options(self) -> None: - """ - Parses arguments that are common for the main Freqtrade, all subcommands and scripts. - """ - 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, - ) - parser.add_argument( - '--logfile', - help='Log to the file specified.', - dest='logfile', - metavar='FILE', - ) - parser.add_argument( - '--version', - action='version', - version=f'%(prog)s {__version__}' - ) - parser.add_argument( - '-c', '--config', - 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', - ) - parser.add_argument( - '-d', '--datadir', - help='Path to backtest data.', - dest='datadir', - metavar='PATH', - ) - - def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None: - """ - Parses arguments common for Backtesting, Edge and Hyperopt modules. - :param parser: - """ - parser = subparser or self.parser - - parser.add_argument( - '-i', '--ticker-interval', - help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', - dest='ticker_interval', - ) - parser.add_argument( - '--timerange', - help='Specify what timerange of data to use.', - dest='timerange', - ) - parser.add_argument( - '--max_open_trades', - help='Specify max_open_trades to use.', - type=int, - dest='max_open_trades', - ) - parser.add_argument( - '--stake_amount', - help='Specify stake_amount.', - type=float, - dest='stake_amount', - ) - parser.add_argument( - '-r', '--refresh-pairs-cached', - help='Refresh the pairs files in tests/testdata with the latest data from the ' - 'exchange. Use it if you want to run your optimization commands with ' - 'up-to-date data.', - action='store_true', - dest='refresh_pairs', - ) - - 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).', - action='store_true', - dest='position_stacking', - default=False - ) - parser.add_argument( - '--dmmp', '--disable-max-market-positions', - help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number).', - action='store_false', - dest='use_max_market_positions', - default=True - ) - parser.add_argument( - '-l', '--live', - help='Use live data.', - action='store_true', - dest='live', - ) - parser.add_argument( - '--strategy-list', - help='Provide a comma-separated list of strategies to backtest. ' - 'Please note that ticker-interval needs to be set either in config ' - 'or via command line. When using this together with `--export trades`, ' - 'the strategy-name is injected into the filename ' - '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', - nargs='+', - dest='strategy_list', - ) - parser.add_argument( - '--export', - help='Export backtest results, argument are: trades. ' - 'Example: `--export=trades`', - dest='export', - ) - parser.add_argument( - '--export-filename', - help='Save backtest results to the file with this filename (default: `%(default)s`). ' - 'Requires `--export` to be set as well. ' - 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', - default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), - dest='exportfilename', - metavar='PATH', - ) - - 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 values against which edge will assess the strategy. ' - 'The format is "min,max,step" (without any space). ' - 'Example: `--stoplosses=-0.01,-0.1,-0.001`', - dest='stoploss_range', - ) - - 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, - metavar='NAME', - ) - parser.add_argument( - '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking).', - action='store_true', - dest='position_stacking', - default=False - ) - parser.add_argument( - '--dmmp', '--disable-max-market-positions', - help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number).', - action='store_false', - dest='use_max_market_positions', - default=True - ) - parser.add_argument( - '-e', '--epochs', - help='Specify number of epochs (default: %(default)d).', - dest='epochs', - default=constants.HYPEROPT_EPOCH, - type=int, - metavar='INT', - ) - parser.add_argument( - '-s', '--spaces', - help='Specify which parameters to hyperopt. Space-separated list. ' - 'Default: `%(default)s`.', - choices=['all', 'buy', 'sell', 'roi', 'stoploss'], - default='all', - nargs='+', - dest='spaces', - ) - parser.add_argument( - '--print-all', - help='Print all results, not only the best ones.', - action='store_true', - dest='print_all', - default=False - ) - parser.add_argument( - '-j', '--job-workers', - help='The number of concurrently running jobs for hyperoptimization ' - '(hyperopt worker processes). ' - 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' - 'If 1 is given, no parallel computing code is used at all.', - dest='hyperopt_jobs', - default=-1, - type=int, - metavar='JOBS', - ) - parser.add_argument( - '--random-state', - help='Set random state to some positive integer for reproducible hyperopt results.', - dest='hyperopt_random_state', - type=check_int_positive, - metavar='INT', - ) - parser.add_argument( - '--min-trades', - help="Set minimal desired number of trades for evaluations in the hyperopt " - "optimization path (default: 1).", - dest='hyperopt_min_trades', - default=1, - type=check_int_positive, - metavar='INT', - ) - - 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.', - action='store_true', - dest='print_one_column', - ) - def _build_subcommands(self) -> None: """ Builds and attaches all subcommands. @@ -731,94 +494,3 @@ class Arguments(object): stop = int(stops) return TimeRange(stype[0], stype[1], start, stop) raise Exception('Incorrect syntax for timerange "%s"' % text) - - def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None: - """ - Parses arguments common for scripts. - """ - parser = subparser or self.parser - - parser.add_argument( - '-p', '--pairs', - help='Show profits for only these pairs. Pairs are comma-separated.', - dest='pairs', - ) - - def download_data_options(self) -> None: - """ - Parses given arguments for testdata download script - """ - parser = self.parser - - parser.add_argument( - '--pairs-file', - help='File containing a list of pairs to download.', - dest='pairs_file', - metavar='FILE', - ) - parser.add_argument( - '--days', - help='Download data for given number of days.', - dest='days', - type=check_int_positive, - metavar='INT', - ) - parser.add_argument( - '--exchange', - help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' - f'Only valid if no config is provided.', - dest='exchange', - ) - parser.add_argument( - '-t', '--timeframes', - 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='+', - dest='timeframes', - ) - 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. ' - 'Comma-separated list. Example: `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. ' - 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', - default='macd,macdsignal', - dest='indicators2', - ) - parser.add_argument( - '--plot-limit', - help='Specify tick limit for plotting. Notice: too high values cause huge files. ' - 'Default: %(default)s.', - dest='plot_limit', - default=750, - type=int, - ) - parser.add_argument( - '--trade-source', - help='Specify the source for trades (Can be DB or file (backtest file)) ' - 'Default: %(default)s', - dest='trade_source', - default="file", - choices=["DB", "file"] - ) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 38383a346..c9d2a2261 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,7 +3,8 @@ import argparse import pytest -from freqtrade.arguments import Arguments, TimeRange, check_int_positive, ARGS_DOWNLOADER +from freqtrade.arguments import (ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME, + Arguments, TimeRange, check_int_positive) # Parse common command-line-arguments. Used for all tools @@ -49,8 +50,8 @@ def test_parse_args_verbose() -> None: def test_common_scripts_options() -> None: arguments = Arguments(['-p', 'ETH/BTC'], '') - arguments.common_scripts_options() - args = arguments.get_parsed_arg() + arguments.build_args(ARGS_DOWNLOADER) + args = arguments.parse_args() assert args.pairs == 'ETH/BTC' @@ -195,8 +196,7 @@ def test_plot_dataframe_options() -> None: '-p', 'UNITTEST/BTC', ] arguments = Arguments(args, '') - arguments.common_scripts_options() - arguments.plot_dataframe_options() + arguments.build_args(ARGS_PLOT_DATAFRAME) pargs = arguments.parse_args(True) assert pargs.indicators1 == "sma10,sma100" assert pargs.indicators2 == "macd,fastd,fastk" From c106534663915bb04038dd83aff836c351cb31f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 20:13:40 +0200 Subject: [PATCH 015/191] Improve developer-document to include a note to keep both branches uptodate while creating a changelog. Cost me ~5 minutes doing the 2019.6 release... --- docs/developer.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index cf6b5d2cd..a5cf837e4 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -130,7 +130,7 @@ If the day shows the same day, then the last candle can be assumed as incomplete This part of the documentation is aimed at maintainers, and shows how to create a release. -### create release branch +### Create release branch ``` bash # make sure you're in develop branch @@ -144,7 +144,10 @@ git checkout -b new_release * Commit this part * push that branch to the remote and create a PR against the master branch -### create changelog from git commits +### Create changelog from git commits + +!!! Note + Make sure that both master and develop are up-todate!. ``` bash # Needs to be done before merging / pulling that branch. From 8e92fc62a39ba4fb60d018b980ab54bcf6774db2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 20:16:31 +0200 Subject: [PATCH 016/191] Use correct new versioning now --- docs/developer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index a5cf837e4..716148788 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -140,7 +140,7 @@ git checkout develop git checkout -b new_release ``` -* Edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`) +* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for June 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * Commit this part * push that branch to the remote and create a PR against the master branch @@ -163,5 +163,5 @@ git log --oneline --no-decorate --no-merges master..develop ### After-release -* Update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`). +* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`). * Create a PR against develop to update that branch. From 353437bbd1f95fae76f489471f1058c2f5869ab8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 21:08:40 +0200 Subject: [PATCH 017/191] 07 is July!! --- docs/developer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer.md b/docs/developer.md index 716148788..f58e0597d 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -140,7 +140,7 @@ git checkout develop git checkout -b new_release ``` -* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for June 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. +* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * Commit this part * push that branch to the remote and create a PR against the master branch From 1d5c3f34ae357badcfdf196ad7571ac4a2835d1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jun 2019 19:59:57 +0200 Subject: [PATCH 018/191] Update qtpylib from source --- freqtrade/vendor/qtpylib/indicators.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index 6edf626f0..b3b2ac533 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -213,8 +213,7 @@ def atr(bars, window=14, exp=False): else: res = rolling_mean(tr, window) - res = pd.Series(res) - return (res.shift(1) * (window - 1) + res) / window + return pd.Series(res) # --------------------------------------------- @@ -602,6 +601,14 @@ def pvt(bars): bars['close'].shift(1)) * bars['volume'] return trend.cumsum() + +def chopiness(bars, window=14): + atrsum = true_range(bars).rolling(window).sum() + highs = bars['high'].rolling(window).max() + lows = bars['low'].rolling(window).min() + return 100 * np.log10(atrsum / (highs - lows)) / np.log10(window) + + # ============================================= @@ -629,6 +636,7 @@ PandasObject.rsi = rsi PandasObject.stoch = stoch PandasObject.zscore = zscore PandasObject.pvt = pvt +PandasObject.chopiness = chopiness PandasObject.tdi = tdi PandasObject.true_range = true_range PandasObject.mid_price = mid_price From 6fc6eaf742823b9a2e53415a4220faa5f35589a3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 26 Jun 2019 22:23:16 +0300 Subject: [PATCH 019/191] minor: couple of typos fixed --- freqtrade/resolvers/strategy_resolver.py | 2 +- freqtrade/tests/strategy/test_strategy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index b2743a417..47218bef4 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -132,7 +132,7 @@ class StrategyResolver(IResolver): abs_paths.insert(0, Path(extra_dir).resolve()) if ":" in strategy_name: - logger.info("loading base64 endocded strategy") + logger.info("loading base64 encoded strategy") strat = strategy_name.split(":") if len(strat) == 2: diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 15d1c18ef..8971b041c 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -66,7 +66,7 @@ def test_load_strategy(result): assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) -def test_load_strategy_byte64(result): +def test_load_strategy_base64(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)}) From 05d93cda16ffb9582e2c8e44ba2022a2b67adb4c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 27 Jun 2019 01:03:38 +0300 Subject: [PATCH 020/191] fix #1963 --- freqtrade/freqtradebot.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b6fc005dd..b3e0ef59d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -478,8 +478,11 @@ class FreqtradeBot(object): return order_amount # use fee from order-dict if possible - if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}): - if trade.pair.startswith(order['fee']['currency']): + if 'fee' in order and order['fee'] is not None and \ + (order['fee'].keys() >= {'currency', 'cost'}): + if order['fee']['currency'] is not None and \ + order['fee']['cost'] is not None and \ + trade.pair.startswith(order['fee']['currency']): new_amount = order_amount - order['fee']['cost'] logger.info("Applying fee on amount for %s (from %s to %s) from Order", trade, order['amount'], new_amount) @@ -496,9 +499,12 @@ class FreqtradeBot(object): fee_abs = 0 for exectrade in trades: amount += exectrade['amount'] - if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}): + if "fee" in exectrade and exectrade['fee'] is not None and \ + (exectrade['fee'].keys() >= {'currency', 'cost'}): # only applies if fee is in quote currency! - if trade.pair.startswith(exectrade['fee']['currency']): + if exectrade['fee']['currency'] is not None and \ + exectrade['fee']['cost'] is not None and \ + trade.pair.startswith(exectrade['fee']['currency']): fee_abs += exectrade['fee']['cost'] if amount != order_amount: From 3043a8d9c9b52310d8c590806b45c5f6a4ae6af0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 06:20:22 +0200 Subject: [PATCH 021/191] Be more explicit about what's missing --- docs/installation.md | 3 +++ setup.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index f0c536ade..d74280c9e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -18,6 +18,9 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. +!!! Note + Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case. + ```bash git clone git@github.com:freqtrade/freqtrade.git cd freqtrade diff --git a/setup.sh b/setup.sh index 8b5531746..c99a98eb1 100755 --- a/setup.sh +++ b/setup.sh @@ -4,7 +4,7 @@ function check_installed_pip() { ${PYTHON} -m pip > /dev/null if [ $? -ne 0 ]; then - echo "pip not found. Please make sure that pip is available for ${PYTHON}." + echo "pip not found (called as '${PYTHON} -m pip'). Please make sure that pip is available for ${PYTHON}." exit 1 fi } From f04d49886bca1d56b0f149cd6a9c47e0a6a6de76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 06:29:18 +0200 Subject: [PATCH 022/191] Add test to verify behaviour if currency in fee-dict is None --- freqtrade/tests/test_freqtradebot.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 87b344853..89d3bba5f 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2886,6 +2886,30 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo assert freqtrade.get_real_amount(trade, buy_order_fee) == amount +def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, mocker): + + limit_buy_order = deepcopy(buy_order_fee) + limit_buy_order['fee'] = {'cost': 0.004, 'currency': None} + trades_for_order[0]['fee']['currency'] = None + + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + amount = sum(x['amount'] for x in trades_for_order) + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + open_order_id="123456" + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Amount does not change + assert freqtrade.get_real_amount(trade, limit_buy_order) == amount + + def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker): trades_for_order[0]['fee']['currency'] = 'BNB' trades_for_order[0]['fee']['cost'] = 0.00094518 From f8dd0b0cb318434e2479460b7177f11525ae9f70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 06:32:26 +0200 Subject: [PATCH 023/191] Use parenteses instead of \ seperators --- freqtrade/freqtradebot.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b3e0ef59d..a62591b15 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -478,11 +478,11 @@ class FreqtradeBot(object): return order_amount # use fee from order-dict if possible - if 'fee' in order and order['fee'] is not None and \ - (order['fee'].keys() >= {'currency', 'cost'}): - if order['fee']['currency'] is not None and \ - order['fee']['cost'] is not None and \ - trade.pair.startswith(order['fee']['currency']): + if ('fee' in order and order['fee'] is not None and + (order['fee'].keys() >= {'currency', 'cost'})): + if (order['fee']['currency'] is not None and + order['fee']['cost'] is not None and + trade.pair.startswith(order['fee']['currency'])): new_amount = order_amount - order['fee']['cost'] logger.info("Applying fee on amount for %s (from %s to %s) from Order", trade, order['amount'], new_amount) @@ -499,12 +499,12 @@ class FreqtradeBot(object): fee_abs = 0 for exectrade in trades: amount += exectrade['amount'] - if "fee" in exectrade and exectrade['fee'] is not None and \ - (exectrade['fee'].keys() >= {'currency', 'cost'}): + if ("fee" in exectrade and exectrade['fee'] is not None and + (exectrade['fee'].keys() >= {'currency', 'cost'})): # only applies if fee is in quote currency! - if exectrade['fee']['currency'] is not None and \ - exectrade['fee']['cost'] is not None and \ - trade.pair.startswith(exectrade['fee']['currency']): + if (exectrade['fee']['currency'] is not None and + exectrade['fee']['cost'] is not None and + trade.pair.startswith(exectrade['fee']['currency'])): fee_abs += exectrade['fee']['cost'] if amount != order_amount: From 98681b78b4deeddc4ac1cc4bf91359cdc1fa5a6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 07:06:11 +0200 Subject: [PATCH 024/191] Show ifferent message for balance in dry-run --- freqtrade/rpc/rpc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 048ebec63..f5adffc65 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -298,7 +298,10 @@ class RPC(object): 'est_btc': est_btc, }) if total == 0.0: - raise RPCException('all balances are zero') + if self._freqtrade.config.get('dry_run', False): + raise RPCException('Running in Dry Run, balances are not available.') + else: + raise RPCException('All balances are zero.') symbol = fiat_display_currency value = self._fiat_converter.convert_amount(total, 'BTC', From 6643b83afe0f92170bb822ad530ca85595897d49 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 07:06:35 +0200 Subject: [PATCH 025/191] Update tests to test both balance versions --- freqtrade/tests/rpc/test_rpc_telegram.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index b34e214af..673536993 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -559,10 +559,32 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: telegram = Telegram(freqtradebot) + freqtradebot.config['dry_run'] = False telegram._balance(bot=MagicMock(), update=update) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert 'all balances are zero' in result + assert 'All balances are zero.' in result + + +def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) + + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + + telegram = Telegram(freqtradebot) + + telegram._balance(bot=MagicMock(), update=update) + result = msg_mock.call_args_list[0][0][0] + assert msg_mock.call_count == 1 + assert "Running in Dry Run, balances are not available." in result def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: From e5a8030dd7883d28797b8797f1b3207fa3623c1f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 27 Jun 2019 16:42:10 +0300 Subject: [PATCH 026/191] comment added --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e0660eacb..d963b39c5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -265,6 +265,7 @@ class Backtesting(object): closerate = - (trade.open_rate * roi + trade.open_rate * (1 + trade.fee_open)) / (trade.fee_close - 1) else: + # This should not be reached... closerate = sell_row.open else: closerate = sell_row.open From 16a9e6b72f54c64496afb4dfd5d4d00b6cdc8596 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 19:51:04 +0200 Subject: [PATCH 027/191] Improve install documentation --- docs/installation.md | 56 +++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index d74280c9e..544706f87 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,12 +4,22 @@ This page explains how to prepare your environment for running the bot. ## Prerequisite +### Requirements + +Click each one for install guide: + +* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) +* [pip](https://pip.pypa.io/en/stable/installing/) +* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below) + +### API keys + 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 recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). -- [Setup your exchange account](#setup-your-exchange-account) - ### Setup your exchange account You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script. @@ -33,7 +43,7 @@ git checkout develop ## Easy Installation - Linux Script -If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot. +If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot. ```bash $ ./setup.sh @@ -48,7 +58,7 @@ usage: This script will install everything you need to run the bot: -* Mandatory software as: `Python3`, `ta-lib`, `wget` +* Mandatory software as: `ta-lib` * Setup your virtualenv * Configure your `config.json` file @@ -73,24 +83,16 @@ Config parameter is a `config.json` configurator. This script will ask you quest We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. -### Requirements - -Click each one for install guide: - -* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) -* [pip](https://pip.pypa.io/en/stable/installing/) -* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) -* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) +!!! Note + Python3.6 or higher and the corresponding pip are assumed to be available. ### Linux - Ubuntu 16.04 -#### Install Python 3.6, Git, and wget +#### Install necessary dependencies ```bash -sudo add-apt-repository ppa:jonathonf/python-3.6 sudo apt-get update -sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git +sudo apt-get install build-essential git ``` #### Raspberry Pi / Raspbian @@ -114,14 +116,6 @@ python3 -m pip install -r requirements-common.txt python3 -m pip install -e . ``` -### MacOS - -#### Install Python 3.6, git and wget - -```bash -brew install python3 git wget -``` - ### Common #### 1. Install TA-Lib @@ -162,7 +156,7 @@ git clone https://github.com/freqtrade/freqtrade.git ``` -Optionally checkout the stable/master branch: +Optionally checkout the master branch to get the latest stable release: ```bash git checkout master @@ -180,9 +174,9 @@ cp config.json.example config.json #### 5. Install python dependencies ``` bash -pip3 install --upgrade pip -pip3 install -r requirements.txt -pip3 install -e . +python3 -m pip install --upgrade pip +python3 -m pip install -r requirements.txt +python3 -m pip install -e . ``` #### 6. Run the Bot @@ -190,7 +184,7 @@ pip3 install -e . If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -python3.6 freqtrade -c config.json +python3 freqtrade -c config.json ``` *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. @@ -220,7 +214,7 @@ when it changes. The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd as the watchdog. -!!! Note +!!! Note The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. ------ @@ -240,8 +234,6 @@ If that is not available on your system, feel free to try the instructions below git clone https://github.com/freqtrade/freqtrade.git ``` -copy paste `config.json` to ``\path\freqtrade-develop\freqtrade` - #### Install ta-lib Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). From 21bf01a24c9569e21b2d38a9aca6b03e84be2652 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 27 Jun 2019 22:29:17 +0300 Subject: [PATCH 028/191] partial freqtradebot cleanup --- freqtrade/freqtradebot.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a62591b15..b103d73a7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -592,13 +592,13 @@ class FreqtradeBot(object): logger.info(' order book asks top %s: %0.8f', i, order_book_rate) sell_rate = order_book_rate - if self.check_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self.check_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -668,7 +668,7 @@ class FreqtradeBot(object): if stoploss_order and stoploss_order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(stoploss_order) - self.notify_sell(trade) + self._notify_sell(trade) return True # Finally we check if stoploss on exchange should be moved up because of trailing. @@ -713,13 +713,15 @@ class FreqtradeBot(object): 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: - stoploss = self.edge.stoploss(trade.pair) - should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.utcnow(), buy, sell, force_stoploss=stoploss) - else: - should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell) + def _check_and_execute_sell(self, trade: Trade, sell_rate: float, + buy: bool, sell: bool) -> bool: + """ + Check and execute sell + """ + should_sell = self.strategy.should_sell( + trade, sell_rate, datetime.utcnow(), buy, sell, + force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 + ) if should_sell.sell_flag: self.execute_sell(trade, sell_rate, should_sell.sell_type) @@ -873,9 +875,9 @@ class FreqtradeBot(object): trade.close_rate_requested = limit trade.sell_reason = sell_reason.value Trade.session.flush() - self.notify_sell(trade) + self._notify_sell(trade) - def notify_sell(self, trade: Trade): + def _notify_sell(self, trade: Trade): """ Sends rpc notification when a sell occured. """ From 4f5e212f87aa9569f80704d74812486ba1598727 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 28 Jun 2019 01:01:51 +0300 Subject: [PATCH 029/191] fix #1978 --- freqtrade/configuration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index d74b712c3..82349700e 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -170,7 +170,7 @@ class Configuration(object): self._load_logging_config(config) # Support for sd_notify - if self.args.sd_notify: + if 'sd_notify' in self.args and self.args.sd_notify: config['internals'].update({'sd_notify': True}) # Add dynamic_whitelist if found @@ -186,7 +186,8 @@ class Configuration(object): '(not applicable with Backtesting and Hyperopt)' ) - if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL: + if ('db_url' in self.args and self.args.db_url and + self.args.db_url != constants.DEFAULT_DB_PROD_URL): config.update({'db_url': self.args.db_url}) logger.info('Parameter --db-url detected ...') From 0436811cf06f44a77f4164d5daa2521a72a99f5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Jun 2019 06:47:40 +0200 Subject: [PATCH 030/191] Use mode OTHER, nto backtesting --- scripts/plot_dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7e81af925..232fb7aad 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -130,7 +130,7 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]: parsed_args = arguments.parse_args() # Load the configuration - config = setup_configuration(parsed_args, RunMode.BACKTEST) + config = setup_configuration(parsed_args, RunMode.OTHER) return config From 044be3b93e0c51c0826a0205219645ed2e4c32f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 16:57:04 +0200 Subject: [PATCH 031/191] Add create_cum_profit column --- freqtrade/arguments.py | 2 +- freqtrade/data/btanalysis.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f020252f8..cf2c52ed6 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -359,7 +359,7 @@ ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + "refresh_pairs", "live"]) ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + - ["pairs", "timerange", "export", "exportfilename"]) + ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) class TimeRange(NamedTuple): diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 5a0dee042..30fd5bc93 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -109,3 +109,15 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & (trades['close_time'] <= dataframe.iloc[-1]['date'])] return trades + + +def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str): + """ + Adds a column `col_name` with the cumulative profit for the given trades array. + """ + df[col_name] = trades.set_index('close_time')['profitperc'].cumsum() + # Set first value to 0 + df.loc[df.iloc[0].name, col_name] = 0 + # FFill to get continuous + df[col_name] = df[col_name].ffill() + return df From e8796e009c47909fef84b4f677cd754b03f9ac6b Mon Sep 17 00:00:00 2001 From: AlexV Date: Sat, 29 Jun 2019 17:20:10 +0200 Subject: [PATCH 032/191] adding bitstamp to list of bad exchanges. --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a65294091..b649a7b65 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -723,7 +723,7 @@ class Exchange(object): def is_exchange_bad(exchange: str) -> bool: - return exchange in ['bitmex'] + return exchange in ['bitmex', 'bitstamp'] def is_exchange_available(exchange: str, ccxt_module=None) -> bool: From edd3fc88256d8322abda7284d35165d3079021db Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 17:19:42 +0200 Subject: [PATCH 033/191] Add test for create_cum_profit --- freqtrade/data/btanalysis.py | 3 +++ freqtrade/tests/data/test_btanalysis.py | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 30fd5bc93..dae891423 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -114,6 +114,9 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str): """ Adds a column `col_name` with the cumulative profit for the given trades array. + :param df: DataFrame with date index + :param trades: DataFrame containing trades (requires columns close_time and profitperc) + :return: Returns df with one additional column, col_name, containing the cumulative profit. """ df[col_name] = trades.set_index('close_time')['profitperc'].cumsum() # Set first value to 0 diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 1cb48393d..4eca73934 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock -from arrow import Arrow import pytest +from arrow import Arrow from pandas import DataFrame, to_datetime -from freqtrade.arguments import TimeRange -from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, +from freqtrade.arguments import TimeRange, Arguments +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, extract_trades_of_period, load_backtest_data, load_trades_from_db) from freqtrade.data.history import load_pair_history, make_testdata_path @@ -74,3 +74,19 @@ 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 + + +def test_create_cum_profit(): + filename = make_testdata_path(None) / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + timerange = Arguments.parse_timerange("20180110-20180112") + + df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + datadir=None, timerange=timerange) + + cum_profits = create_cum_profit(df.set_index('date'), + bt_data[bt_data["pair"] == 'POWR/BTC'], + "cum_profits") + assert "cum_profits" in cum_profits.columns + assert cum_profits.iloc[0]['cum_profits'] == 0 + assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 From 79b4e2dc8596bd0424e551e29b9f333f5d099439 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 17:23:03 +0200 Subject: [PATCH 034/191] Rename generate_graph to generate_candlestick_graph --- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/test_plotting.py | 14 +++++++------- scripts/plot_dataframe.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index c058f7fb2..ae9889975 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -86,7 +86,7 @@ def plot_trades(fig, trades: pd.DataFrame): return fig -def generate_graph( +def generate_candlestick_graph( pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index ec81b93b8..46462bd76 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -8,7 +8,7 @@ from copy import deepcopy 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, +from freqtrade.plot.plotting import (generate_candlestick_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 @@ -95,7 +95,7 @@ def test_plot_trades(caplog): assert trade_sell.marker.color == 'red' -def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): +def test_generate_candlestick_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', @@ -110,8 +110,8 @@ def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): indicators1 = [] indicators2 = [] - fig = generate_graph(pair=pair, data=data, trades=None, - indicators1=indicators1, indicators2=indicators2) + fig = generate_candlestick_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 @@ -131,7 +131,7 @@ def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): assert log_has("No sell-signals found.", caplog.record_tuples) -def test_generate_graph_no_trades(default_conf, mocker): +def test_generate_candlestick_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', @@ -147,8 +147,8 @@ def test_generate_graph_no_trades(default_conf, mocker): indicators1 = [] indicators2 = [] - fig = generate_graph(pair=pair, data=data, trades=None, - indicators1=indicators1, indicators2=indicators2) + fig = generate_candlestick_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 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 232fb7aad..80773b3b0 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -36,7 +36,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import (extract_trades_of_period, load_backtest_data, load_trades_from_db) from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import generate_graph, generate_plot_file +from freqtrade.plot.plotting import generate_candlestick_graph, generate_plot_file from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -105,7 +105,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) - fig = generate_graph( + fig = generate_candlestick_graph( pair=pair, data=dataframe, trades=trades, From 4506832925717d59b2d307a296996768efa3da8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:07:25 +0200 Subject: [PATCH 035/191] Update docstring --- scripts/plot_dataframe.py | 14 +------------- scripts/plot_profit.py | 10 +--------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 80773b3b0..701672f29 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -2,19 +2,7 @@ """ Script to display when the bot will buy on specific pair(s) -Mandatory Cli parameters: --p / --pairs: pair(s) to examine - -Option but recommended --s / --strategy: strategy to use - - -Optional Cli parameters --d / --datadir: path to pair(s) backtest data ---timerange: specify what timerange of data to use. --l / --live: Live, to download the latest ticker for the pair(s) --db / --db-url: Show trades stored in database - +Use `python plot_dataframe.py --help` to display the command line arguments Indicators recommended Row 1: sma, ema3, ema5, ema10, ema50 diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 32bfae9cc..01dc260d9 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -2,15 +2,7 @@ """ Script to display profits -Mandatory Cli parameters: --p / --pair: pair to examine - -Optional Cli parameters --c / --config: specify configuration file --s / --strategy: strategy to use --d / --datadir: path to pair backtest data ---timerange: specify what timerange of data to use ---export-filename: Specify where the backtest export is located. +Use `python plot_profit.py --help` to display the command line arguments """ import json import logging From e50eee59cf5c5450ff035eaabce0c025dd1580ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:30:31 +0200 Subject: [PATCH 036/191] Seperate plot-name generation and plotting --- freqtrade/plot/plotting.py | 19 ++++++++++++------- freqtrade/tests/test_plotting.py | 17 ++++++++++++----- scripts/plot_dataframe.py | 6 ++++-- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ae9889975..f8a010068 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -204,7 +204,16 @@ def generate_candlestick_graph( return fig -def generate_plot_file(fig, pair, ticker_interval) -> None: +def generate_plot_filename(pair, ticker_interval) -> str: + pair_name = pair.replace("/", "_") + file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' + + logger.info('Generate plot file for %s', pair) + + return file_name + + +def generate_plot_file(fig, filename: str, auto_open: bool = False) -> None: """ Generate a plot html file from pre populated fig plotly object :param fig: Plotly Figure to plot @@ -212,12 +221,8 @@ def generate_plot_file(fig, pair, ticker_interval) -> None: :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) + plot(fig, filename=str(Path('user_data/plots').joinpath(filename)), + auto_open=auto_open) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 46462bd76..527534522 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -1,15 +1,17 @@ +from copy import deepcopy from unittest.mock import MagicMock -from plotly import tools import plotly.graph_objs as go -from copy import deepcopy +from plotly import tools from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_backtest_data -from freqtrade.plot.plotting import (generate_candlestick_graph, generate_plot_file, - generate_row, plot_trades) +from freqtrade.plot.plotting import (generate_candlestick_graph, + generate_plot_file, + generate_plot_filename, generate_row, + plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re @@ -178,10 +180,15 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker): assert trades_mock.call_count == 1 +def test_generate_Plot_filename(): + fn = generate_plot_filename("UNITTEST/BTC", "5m") + assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html" + + 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") + generate_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html") assert plot_mock.call_count == 1 assert plot_mock.call_args[0][0] == fig diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 701672f29..d97c6f041 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -24,7 +24,9 @@ from freqtrade.data import history from freqtrade.data.btanalysis import (extract_trades_of_period, load_backtest_data, load_trades_from_db) from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import generate_candlestick_graph, generate_plot_file +from freqtrade.plot.plotting import (generate_candlestick_graph, + generate_plot_file, + generate_plot_filename) from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -101,7 +103,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): indicators2=config["indicators2"].split(",") ) - generate_plot_file(fig, pair, ticker_interval) + generate_plot_file(fig, generate_plot_filename(pair, ticker_interval)) logger.info('End of ploting process %s plots generated', pair_counter) From 4218d569de7dfa3e400d0f4f7c2dd4698c6961f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:41:22 +0200 Subject: [PATCH 037/191] Only read trades once --- scripts/plot_dataframe.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index d97c6f041..07079304e 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -80,6 +80,11 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): live=config.get("live", False), ) + if config["trade_source"] == "DB": + trades = load_trades_from_db(config["db_url"]) + elif config["trade_source"] == "file": + trades = load_backtest_data(Path(config["exportfilename"])) + pair_counter = 0 for pair, data in tickers.items(): pair_counter += 1 @@ -87,18 +92,14 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): tickers = {} tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - if config["trade_source"] == "DB": - trades = load_trades_from_db(config["db_url"]) - elif config["trade_source"] == "file": - trades = load_backtest_data(Path(config["exportfilename"])) - trades = trades.loc[trades['pair'] == pair] - trades = extract_trades_of_period(dataframe, trades) + trades_pair = trades.loc[trades['pair'] == pair] + trades_pair = extract_trades_of_period(dataframe, trades_pair) fig = generate_candlestick_graph( pair=pair, data=dataframe, - trades=trades, + trades=trades_pair, indicators1=config["indicators1"].split(","), indicators2=config["indicators2"].split(",") ) From 8aa327cb8a331119a919d217e3ea29beadad08b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:50:31 +0200 Subject: [PATCH 038/191] Add load_trades abstraction (to load trades from either DB or file) --- freqtrade/data/btanalysis.py | 14 ++++++++++++++ freqtrade/tests/data/test_btanalysis.py | 25 +++++++++++++++++++++++-- scripts/plot_dataframe.py | 8 ++------ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index dae891423..f6f7e5541 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -101,6 +101,20 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: return trades +def load_trades(config) -> pd.DataFrame: + """ + Based on configuration option "trade_source": + * loads data from DB (using `db_url`) + * loads data from backtestfile (`using exportfilename`) + """ + if config["trade_source"] == "DB": + return load_trades_from_db(config["db_url"]) + elif config["trade_source"] == "file": + return load_backtest_data(Path(config["exportfilename"])) + else: + return None + + 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 diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 4eca73934..01e5dc90d 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -4,10 +4,11 @@ import pytest from arrow import Arrow from pandas import DataFrame, to_datetime -from freqtrade.arguments import TimeRange, Arguments +from freqtrade.arguments import Arguments, TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, extract_trades_of_period, - load_backtest_data, load_trades_from_db) + load_backtest_data, load_trades, + load_trades_from_db) from freqtrade.data.history import load_pair_history, make_testdata_path from freqtrade.tests.test_persistence import create_mock_trades @@ -76,6 +77,26 @@ def test_extract_trades_of_period(): assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime +def test_load_trades(default_conf, mocker): + db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock()) + bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock()) + + default_conf['trade_source'] = "DB" + load_trades(default_conf) + + assert db_mock.call_count == 1 + assert bt_mock.call_count == 0 + + db_mock.reset_mock() + bt_mock.reset_mock() + default_conf['trade_source'] = "file" + default_conf['exportfilename'] = "testfile.json" + load_trades(default_conf) + + assert db_mock.call_count == 0 + assert bt_mock.call_count == 1 + + def test_create_cum_profit(): filename = make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 07079304e..dfd31e9ab 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -21,8 +21,7 @@ import pandas as pd from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments from freqtrade.data import history -from freqtrade.data.btanalysis import (extract_trades_of_period, - load_backtest_data, load_trades_from_db) +from freqtrade.data.btanalysis import extract_trades_of_period, load_trades from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (generate_candlestick_graph, generate_plot_file, @@ -80,10 +79,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): live=config.get("live", False), ) - if config["trade_source"] == "DB": - trades = load_trades_from_db(config["db_url"]) - elif config["trade_source"] == "file": - trades = load_backtest_data(Path(config["exportfilename"])) + trades = load_trades(config) pair_counter = 0 for pair, data in tickers.items(): From c3db4ebbc3ed4fff2206d508633c88aabdec06c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:52:33 +0200 Subject: [PATCH 039/191] Revise plot_profit to use pandas functions where possible --- scripts/plot_profit.py | 145 ++++++++++------------------------------- 1 file changed, 35 insertions(+), 110 deletions(-) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 01dc260d9..5bff9b2dd 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -4,64 +4,28 @@ Script to display profits Use `python plot_profit.py --help` to display the command line arguments """ -import json import logging import sys from argparse import Namespace from pathlib import Path -from typing import List, Optional +from typing import List -import numpy as np +import pandas as pd import plotly.graph_objs as go from plotly import tools from plotly.offline import plot -from freqtrade.arguments import Arguments, ARGS_PLOT_PROFIT +from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments from freqtrade.configuration import Configuration from freqtrade.data import history -from freqtrade.exchange import timeframe_to_seconds -from freqtrade.misc import common_datearray +from freqtrade.data.btanalysis import create_cum_profit, load_trades +from freqtrade.plot.plotting import generate_plot_file from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode - logger = logging.getLogger(__name__) -# data:: [ pair, profit-%, enter, exit, time, duration] -# data:: ["ETH/BTC", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65] -def make_profit_array(data: List, px: int, min_date: int, - interval: str, - filter_pairs: Optional[List] = None) -> np.ndarray: - pg = np.zeros(px) - filter_pairs = filter_pairs or [] - # Go through the trades - # and make an total profit - # array - for trade in data: - pair = trade[0] - if filter_pairs and pair not in filter_pairs: - continue - profit = trade[1] - trade_sell_time = int(trade[3]) - - ix = define_index(min_date, trade_sell_time, interval) - if ix < px: - logger.debug('[%s]: Add profit %s on %s', pair, profit, trade[4]) - pg[ix] += profit - - # rewrite the pg array to go from - # total profits at each timeframe - # to accumulated profits - pa = 0 - for x in range(0, len(pg)): - p = pg[x] # Get current total percent - pa += p # Add to the accumulated percent - pg[x] = pa # write back to save memory - - return pg - - def plot_profit(args: Namespace) -> None: """ Plots the total profit for all pairs. @@ -70,34 +34,15 @@ def plot_profit(args: Namespace) -> None: in helping out to find a good algorithm. """ - # We need to use the same pairs, same ticker_interval - # and same timeperiod as used in backtesting - # to match the tickerdata against the profits-results + # We need to use the same pairs and the same ticker_interval + # as used in backtesting / trading + # to match the tickerdata against the results timerange = Arguments.parse_timerange(args.timerange) config = Configuration(args, RunMode.OTHER).get_config() # Init strategy - try: - strategy = StrategyResolver({'strategy': config.get('strategy')}).strategy - - except AttributeError: - logger.critical( - 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"', - config.get('strategy') - ) - exit(1) - - # Load the profits results - try: - filename = args.exportfilename - with open(filename) as file: - data = json.load(file) - except FileNotFoundError: - logger.critical( - 'File "backtest-result.json" not found. This script require backtesting ' - 'results to run.\nPlease run a backtesting with the parameter --export.') - exit(1) + strategy = StrategyResolver(config).strategy # Take pairs from the cli otherwise switch to the pair in the config file if args.pairs: @@ -106,6 +51,11 @@ def plot_profit(args: Namespace) -> None: else: filter_pairs = config['exchange']['pair_whitelist'] + # Load the profits results + trades = load_trades(config) + + trades = trades[trades['pair'].isin(filter_pairs)] + ticker_interval = strategy.ticker_interval pairs = config['exchange']['pair_whitelist'] @@ -120,49 +70,28 @@ def plot_profit(args: Namespace) -> None: refresh_pairs=False, timerange=timerange ) - dataframes = strategy.tickerdata_to_dataframe(tickers) - # NOTE: the dataframes are of unequal length, - # 'dates' is an merged date array of them all. - - dates = common_datearray(dataframes) - min_date = int(min(dates).timestamp()) - max_date = int(max(dates).timestamp()) - num_iterations = define_index(min_date, max_date, ticker_interval) + 1 - - # Make an average close price of all the pairs that was involved. + # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - # We are essentially saying: - # array <- sum dataframes[*]['close'] / num_items dataframes - # FIX: there should be some onliner numpy/panda for this - avgclose = np.zeros(num_iterations) - num = 0 - for pair, pair_data in dataframes.items(): - close = pair_data['close'] - maxprice = max(close) # Normalize price to [0,1] - logger.info('Pair %s has length %s' % (pair, len(close))) - for x in range(0, len(close)): - avgclose[x] += close[x] / maxprice - # avgclose += close - num += 1 - avgclose /= num - # make an profits-growth array - pg = make_profit_array(data, num_iterations, min_date, ticker_interval, filter_pairs) + # Combine close-values for all pairs, rename columns to "pair" + df_comb = pd.concat([tickers[pair].set_index('date').rename( + {'close': pair}, axis=1)[pair] for pair in tickers], axis=1) + df_comb['mean'] = df_comb.mean(axis=1) + + # Add combined cumulative profit + df_comb = create_cum_profit(df_comb, trades, 'cum_profit') - # # Plot the pairs average close prices, and total profit growth - # - avgclose = go.Scattergl( - x=dates, - y=avgclose, + x=df_comb.index, + y=df_comb['mean'], name='Avg close price', ) profit = go.Scattergl( - x=dates, - y=pg, + x=df_comb.index, + y=df_comb['cum_profit'], name='Profit', ) @@ -172,23 +101,19 @@ def plot_profit(args: Namespace) -> None: fig.append_trace(profit, 2, 1) for pair in pairs: - pg = make_profit_array(data, num_iterations, min_date, ticker_interval, [pair]) + profit_col = f'cum_profit_{pair}' + df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) + pair_profit = go.Scattergl( - x=dates, - y=pg, - name=pair, + x=df_comb.index, + y=df_comb[profit_col], + name=f"Profit {pair}", ) fig.append_trace(pair_profit, 3, 1) - plot(fig, filename=str(Path('user_data').joinpath('freqtrade-profit-plot.html'))) - - -def define_index(min_date: int, max_date: int, ticker_interval: str) -> int: - """ - Return the index of a specific date - """ - interval_seconds = timeframe_to_seconds(ticker_interval) - return int((max_date - min_date) / interval_seconds) + generate_plot_file(fig, + filename='freqtrade-profit-plot.html', + auto_open=True) def plot_parse_args(args: List[str]) -> Namespace: From 700bab7279bd37d97528123d60b43dfba8b9e882 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:28:34 +0200 Subject: [PATCH 040/191] Rename generate_plot_file to store_plot_file --- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/test_plotting.py | 4 ++-- scripts/plot_dataframe.py | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f8a010068..68d61bb92 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -213,7 +213,7 @@ def generate_plot_filename(pair, ticker_interval) -> str: return file_name -def generate_plot_file(fig, filename: str, auto_open: bool = False) -> None: +def store_plot_file(fig, filename: str, auto_open: bool = False) -> None: """ Generate a plot html file from pre populated fig plotly object :param fig: Plotly Figure to plot diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 527534522..0e93e8fad 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -9,7 +9,7 @@ from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_backtest_data from freqtrade.plot.plotting import (generate_candlestick_graph, - generate_plot_file, + store_plot_file, generate_plot_filename, generate_row, plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy @@ -188,7 +188,7 @@ def test_generate_Plot_filename(): def test_generate_plot_file(mocker, caplog): fig = generage_empty_figure() plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) - generate_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html") + store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html") assert plot_mock.call_count == 1 assert plot_mock.call_args[0][0] == fig diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index dfd31e9ab..a0f4ef778 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -24,7 +24,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import extract_trades_of_period, load_trades from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (generate_candlestick_graph, - generate_plot_file, + store_plot_file, generate_plot_filename) from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -67,7 +67,6 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): # Set timerange to use timerange = Arguments.parse_timerange(config["timerange"]) - ticker_interval = strategy.ticker_interval tickers = history.load_data( datadir=Path(str(config.get("datadir"))), @@ -100,7 +99,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): indicators2=config["indicators2"].split(",") ) - generate_plot_file(fig, generate_plot_filename(pair, ticker_interval)) + store_plot_file(fig, generate_plot_filename(pair, config['ticker_interval'])) logger.info('End of ploting process %s plots generated', pair_counter) From c87d27048bbd8e5c53bec3d49f0336dec1953dfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:28:49 +0200 Subject: [PATCH 041/191] align plot_profit to plot_dataframe --- scripts/plot_profit.py | 69 +++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 5bff9b2dd..f28763077 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -6,27 +6,25 @@ Use `python plot_profit.py --help` to display the command line arguments """ import logging import sys -from argparse import Namespace from pathlib import Path -from typing import List +from typing import Any, Dict, List import pandas as pd import plotly.graph_objs as go from plotly import tools -from plotly.offline import plot from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments -from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_trades -from freqtrade.plot.plotting import generate_plot_file -from freqtrade.resolvers import StrategyResolver +from freqtrade.optimize import setup_configuration +from freqtrade.plot.plotting import store_plot_file +from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def plot_profit(args: Namespace) -> None: +def plot_profit(config: Dict[str, Any]) -> None: """ Plots the total profit for all pairs. Note, the profit calculation isn't realistic. @@ -34,42 +32,33 @@ def plot_profit(args: Namespace) -> None: in helping out to find a good algorithm. """ + exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange + + # Take pairs from the cli otherwise switch to the pair in the config file + if "pairs" in config: + pairs = config["pairs"].split(',') + else: + pairs = config["exchange"]["pair_whitelist"] + # We need to use the same pairs and the same ticker_interval # as used in backtesting / trading # to match the tickerdata against the results - timerange = Arguments.parse_timerange(args.timerange) + timerange = Arguments.parse_timerange(config["timerange"]) - config = Configuration(args, RunMode.OTHER).get_config() - - # Init strategy - strategy = StrategyResolver(config).strategy - - # Take pairs from the cli otherwise switch to the pair in the config file - if args.pairs: - filter_pairs = args.pairs - filter_pairs = filter_pairs.split(',') - else: - filter_pairs = config['exchange']['pair_whitelist'] + 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), + ) # Load the profits results trades = load_trades(config) - trades = trades[trades['pair'].isin(filter_pairs)] - - ticker_interval = strategy.ticker_interval - pairs = config['exchange']['pair_whitelist'] - - if filter_pairs: - pairs = list(set(pairs) & set(filter_pairs)) - logger.info('Filter, keep pairs %s' % pairs) - - tickers = history.load_data( - datadir=Path(str(config.get('datadir'))), - pairs=pairs, - ticker_interval=ticker_interval, - refresh_pairs=False, - timerange=timerange - ) + trades = trades[trades['pair'].isin(pairs)] # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend @@ -111,12 +100,12 @@ def plot_profit(args: Namespace) -> None: ) fig.append_trace(pair_profit, 3, 1) - generate_plot_file(fig, + store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) -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 @@ -125,7 +114,11 @@ def plot_parse_args(args: List[str]) -> Namespace: arguments = Arguments(args, 'Graph profits') arguments.build_args(optionlist=ARGS_PLOT_PROFIT) - return arguments.parse_args() + parsed_args = arguments.parse_args() + + # Load the configuration + config = setup_configuration(parsed_args, RunMode.OTHER) + return config def main(sysargv: List[str]) -> None: From 42ea0a19d2b79e319398569aa543c84ac46c3e92 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:41:43 +0200 Subject: [PATCH 042/191] create FTPlots class to combine duplicate script code --- freqtrade/plot/plotting.py | 42 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 68d61bb92..868ffbc31 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,8 +1,14 @@ import logging -from typing import List +from pathlib import Path +from typing import Any, Dict, List, Optional import pandas as pd -from pathlib import Path + +from freqtrade.arguments import Arguments +from frqtrade.exchange import Exchange +from freqtrade.data import history +from freqtrade.data.btanalysis import load_trades +from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) @@ -16,6 +22,38 @@ except ImportError: exit(1) +class FTPlots(): + + def __init__(self, config: Dict[str, Any]): + self._config = config + self.exchange: Optional[Exchange] = None + + if self._config.get("live", False) or self._config.get("refresh_pairs", False): + self.exchange = ExchangeResolver(self._config.get('exchange', {}).get('name'), + self._config).exchange + + self.strategy = StrategyResolver(self._config).strategy + if "pairs" in self._config: + self.pairs = self._config["pairs"].split(',') + else: + self.pairs = self._config["exchange"]["pair_whitelist"] + + # Set timerange to use + self.timerange = Arguments.parse_timerange(self._config["timerange"]) + + self.tickers = history.load_data( + datadir=Path(str(self._config.get("datadir"))), + pairs=self.pairs, + ticker_interval=self._config['ticker_interval'], + refresh_pairs=self._config.get('refresh_pairs', False), + timerange=self.timerange, + exchange=self.exchange, + live=self._config.get("live", False), + ) + + self.trades = load_trades(self._config) + + 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 From 88545d882ca7df3b52586b0058ad9bf94fc0fe2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:42:10 +0200 Subject: [PATCH 043/191] Use FTPlots class in plot-scripts --- scripts/plot_dataframe.py | 36 +++++------------------------- scripts/plot_profit.py | 46 +++++++-------------------------------- 2 files changed, 14 insertions(+), 68 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index a0f4ef778..431c6239c 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -14,19 +14,16 @@ Example of usage: """ import logging import sys -from pathlib import Path from typing import Any, Dict, List import pandas as pd from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments -from freqtrade.data import history -from freqtrade.data.btanalysis import extract_trades_of_period, load_trades +from freqtrade.data.btanalysis import extract_trades_of_period from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import (generate_candlestick_graph, +from freqtrade.plot.plotting import (FTPlots, generate_candlestick_graph, store_plot_file, generate_plot_filename) -from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -57,38 +54,17 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): -Generate plot files :return: None """ - exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange - - strategy = StrategyResolver(config).strategy - if "pairs" in config: - pairs = config["pairs"].split(',') - else: - pairs = config["exchange"]["pair_whitelist"] - - # Set timerange to use - timerange = Arguments.parse_timerange(config["timerange"]) - - 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), - ) - - trades = load_trades(config) + plot = FTPlots(config) pair_counter = 0 - for pair, data in tickers.items(): + for pair, data in plot.tickers.items(): pair_counter += 1 logger.info("analyse pair %s", pair) tickers = {} tickers[pair] = data - dataframe = generate_dataframe(strategy, tickers, pair) + dataframe = generate_dataframe(plot.strategy, tickers, pair) - trades_pair = trades.loc[trades['pair'] == pair] + trades_pair = plot.trades.loc[plot.trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) fig = generate_candlestick_graph( diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index f28763077..cd507100f 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -6,7 +6,6 @@ Use `python plot_profit.py --help` to display the command line arguments """ import logging import sys -from pathlib import Path from typing import Any, Dict, List import pandas as pd @@ -14,11 +13,9 @@ import plotly.graph_objs as go from plotly import tools from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments -from freqtrade.data import history -from freqtrade.data.btanalysis import create_cum_profit, load_trades +from freqtrade.data.btanalysis import create_cum_profit from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import store_plot_file -from freqtrade.resolvers import ExchangeResolver +from freqtrade.plot.plotting import FTPlots, store_plot_file from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -31,41 +28,16 @@ def plot_profit(config: Dict[str, Any]) -> None: But should be somewhat proportional, and therefor useful in helping out to find a good algorithm. """ + plot = FTPlots(config) - exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange - - # Take pairs from the cli otherwise switch to the pair in the config file - if "pairs" in config: - pairs = config["pairs"].split(',') - else: - pairs = config["exchange"]["pair_whitelist"] - - # We need to use the same pairs and the same ticker_interval - # as used in backtesting / trading - # to match the tickerdata against the results - timerange = Arguments.parse_timerange(config["timerange"]) - - 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), - ) - - # Load the profits results - trades = load_trades(config) - - trades = trades[trades['pair'].isin(pairs)] + trades = plot.trades[plot.trades['pair'].isin(plot.pairs)] # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend # Combine close-values for all pairs, rename columns to "pair" - df_comb = pd.concat([tickers[pair].set_index('date').rename( - {'close': pair}, axis=1)[pair] for pair in tickers], axis=1) + df_comb = pd.concat([plot.tickers[pair].set_index('date').rename( + {'close': pair}, axis=1)[pair] for pair in plot.tickers], axis=1) df_comb['mean'] = df_comb.mean(axis=1) # Add combined cumulative profit @@ -89,7 +61,7 @@ def plot_profit(config: Dict[str, Any]) -> None: fig.append_trace(avgclose, 1, 1) fig.append_trace(profit, 2, 1) - for pair in pairs: + for pair in plot.pairs: profit_col = f'cum_profit_{pair}' df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) @@ -100,9 +72,7 @@ def plot_profit(config: Dict[str, Any]) -> None: ) fig.append_trace(pair_profit, 3, 1) - store_plot_file(fig, - filename='freqtrade-profit-plot.html', - auto_open=True) + store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) def plot_parse_args(args: List[str]) -> Dict[str, Any]: From 0d5e94b147b74a201914b2558e69a5cf2470bd9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:44:50 +0200 Subject: [PATCH 044/191] Rename generate_row to add_indicators --- freqtrade/plot/plotting.py | 6 +++--- freqtrade/tests/test_plotting.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 868ffbc31..8ac4800bb 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -54,7 +54,7 @@ class FTPlots(): self.trades = load_trades(self._config) -def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: +def add_indicators(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 @@ -224,7 +224,7 @@ def generate_candlestick_graph( fig.append_trace(bb_upper, 1, 1) # Add indicators to main plot - fig = generate_row(fig=fig, row=1, indicators=indicators1, data=data) + fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data) fig = plot_trades(fig, trades) @@ -237,7 +237,7 @@ def generate_candlestick_graph( fig.append_trace(volume, 2, 1) # Add indicators to seperate row - fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data) + fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data) return fig diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 0e93e8fad..e4d913f70 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -10,14 +10,14 @@ from freqtrade.data import history from freqtrade.data.btanalysis import load_backtest_data from freqtrade.plot.plotting import (generate_candlestick_graph, store_plot_file, - generate_plot_filename, generate_row, + generate_plot_filename, add_indicators, 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 - used to mock add_indicators and plot_trades""" return fig @@ -36,7 +36,7 @@ def generage_empty_figure(): ) -def test_generate_row(default_conf, caplog): +def test_add_indicators(default_conf, caplog): pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) @@ -51,20 +51,20 @@ def test_generate_row(default_conf, caplog): fig = generage_empty_figure() # Row 1 - fig1 = generate_row(fig=deepcopy(fig), row=1, indicators=indicators1, data=data) + fig1 = add_indicators(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) + fig2 = add_indicators(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) + fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) assert fig == fig3 assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples) @@ -98,7 +98,7 @@ def test_plot_trades(caplog): def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, caplog): - row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', + row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators', MagicMock(side_effect=fig_generating_mock)) trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', MagicMock(side_effect=fig_generating_mock)) @@ -134,7 +134,7 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, c def test_generate_candlestick_graph_no_trades(default_conf, mocker): - row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', + row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators', MagicMock(side_effect=fig_generating_mock)) trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', MagicMock(side_effect=fig_generating_mock)) From 348513c1514311894035642591d046d035c73df3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:47:07 +0200 Subject: [PATCH 045/191] Improve formatting of plotting.py --- freqtrade/plot/plotting.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 8ac4800bb..2fe15d320 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -84,7 +84,7 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools def plot_trades(fig, trades: pd.DataFrame): """ - Plot trades to "fig" + Add trades to "fig" """ # Trades can be empty if trades is not None and len(trades) > 0: @@ -124,13 +124,9 @@ def plot_trades(fig, trades: pd.DataFrame): return fig -def generate_candlestick_graph( - pair: str, - data: pd.DataFrame, - trades: pd.DataFrame = None, - indicators1: List[str] = [], - indicators2: List[str] = [], -) -> go.Figure: +def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, + indicators1: List[str] = [], + indicators2: List[str] = [],) -> 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 3 are to our disposal for custom indicators @@ -243,6 +239,9 @@ def generate_candlestick_graph( def generate_plot_filename(pair, ticker_interval) -> str: + """ + Generate filenames per pair/ticker_interval to be used for storing plots + """ pair_name = pair.replace("/", "_") file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' From 6b387d320e594bd8c82bd91cca951a6a09dc3050 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:04:43 +0200 Subject: [PATCH 046/191] extract combine_tickers to btanalysis --- freqtrade/data/btanalysis.py | 19 ++++++++++++++++++- freqtrade/tests/data/test_btanalysis.py | 20 ++++++++++++++++++-- scripts/plot_profit.py | 6 ++---- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f6f7e5541..834d41263 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -3,6 +3,7 @@ Helpers when analyzing backtest data """ import logging from pathlib import Path +from typing import Dict import numpy as np import pandas as pd @@ -125,7 +126,23 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p return trades -def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str): +def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "close"): + """ + Combine multiple dataframes "column" + :param tickers: Dict of Dataframes, dict key should be pair. + :param column: Column in the original dataframes to use + :return: DataFrame with the column renamed to the dict key, and a column + named mean, containing the mean of all pairs. + """ + df_comb = pd.concat([tickers[pair].set_index('date').rename( + {column: pair}, axis=1)[pair] for pair in tickers], axis=1) + + df_comb['mean'] = df_comb.mean(axis=1) + + return df_comb + + +def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame: """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 01e5dc90d..e8872f9a4 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -5,11 +5,14 @@ from arrow import Arrow from pandas import DataFrame, to_datetime from freqtrade.arguments import Arguments, TimeRange -from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, + combine_tickers_with_mean, + create_cum_profit, extract_trades_of_period, load_backtest_data, load_trades, load_trades_from_db) -from freqtrade.data.history import load_pair_history, make_testdata_path +from freqtrade.data.history import (load_data, load_pair_history, + make_testdata_path) from freqtrade.tests.test_persistence import create_mock_trades @@ -97,6 +100,19 @@ def test_load_trades(default_conf, mocker): assert bt_mock.call_count == 1 +def test_combine_tickers_with_mean(): + pairs = ["ETH/BTC", "XLM/BTC"] + tickers = load_data(datadir=None, + pairs=pairs, + ticker_interval='5m' + ) + df = combine_tickers_with_mean(tickers) + assert isinstance(df, DataFrame) + assert "ETH/BTC" in df.columns + assert "XLM/BTC" in df.columns + assert "mean" in df.columns + + def test_create_cum_profit(): filename = make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index cd507100f..248eeb7b0 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -13,7 +13,7 @@ import plotly.graph_objs as go from plotly import tools from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments -from freqtrade.data.btanalysis import create_cum_profit +from freqtrade.data.btanalysis import create_cum_profit, combine_tickers_with_mean from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import FTPlots, store_plot_file from freqtrade.state import RunMode @@ -36,9 +36,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # this could be useful to gauge the overall market trend # Combine close-values for all pairs, rename columns to "pair" - df_comb = pd.concat([plot.tickers[pair].set_index('date').rename( - {'close': pair}, axis=1)[pair] for pair in plot.tickers], axis=1) - df_comb['mean'] = df_comb.mean(axis=1) + df_comb = combine_tickers_with_mean(plot.tickers, "close") # Add combined cumulative profit df_comb = create_cum_profit(df_comb, trades, 'cum_profit') From 0a184d380e0c0c045f7a4539da100fade9db51a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:14:33 +0200 Subject: [PATCH 047/191] create add_profit function --- freqtrade/plot/plotting.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 2fe15d320..f8736fe74 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -5,9 +5,9 @@ from typing import Any, Dict, List, Optional import pandas as pd from freqtrade.arguments import Arguments -from frqtrade.exchange import Exchange +from freqtrade.exchange import Exchange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades +from freqtrade.data.btanalysis import load_trades, create_cum_profit from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) @@ -82,7 +82,27 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools return fig -def plot_trades(fig, trades: pd.DataFrame): +def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> tools.make_subplots: + """ + Add profit-plot + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param column: Column to use for plot + :param name: Name to use + :return: fig with added profit plot + """ + profit = go.Scattergl( + x=data.index, + y=data[column], + name=name, + ) + fig.append_trace(profit, row, 1) + + return fig + + +def plot_trades(fig, trades: pd.DataFrame) -> tools.make_subplots: """ Add trades to "fig" """ From 5a11ffcad808ede0990c72356e8e19c627aca0e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:24:10 +0200 Subject: [PATCH 048/191] Add test for add_profit --- freqtrade/data/btanalysis.py | 2 -- freqtrade/tests/test_plotting.py | 26 +++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 834d41263..556666f4e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -112,8 +112,6 @@ def load_trades(config) -> pd.DataFrame: return load_trades_from_db(config["db_url"]) elif config["trade_source"] == "file": return load_backtest_data(Path(config["exportfilename"])) - else: - return None def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index e4d913f70..2a73ad24e 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,11 +5,11 @@ from unittest.mock import MagicMock import plotly.graph_objs as go from plotly import tools -from freqtrade.arguments import TimeRange +from freqtrade.arguments import TimeRange, Arguments from freqtrade.data import history -from freqtrade.data.btanalysis import load_backtest_data +from freqtrade.data.btanalysis import load_backtest_data, create_cum_profit from freqtrade.plot.plotting import (generate_candlestick_graph, - store_plot_file, + store_plot_file, add_profit, generate_plot_filename, add_indicators, plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy @@ -194,3 +194,23 @@ def test_generate_plot_file(mocker, caplog): assert plot_mock.call_args[0][0] == fig assert (plot_mock.call_args_list[0][1]['filename'] == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") + + +def test_add_profit(): + filename = history.make_testdata_path(None) / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + timerange = Arguments.parse_timerange("20180110-20180112") + + df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m', + datadir=None, timerange=timerange) + fig = generage_empty_figure() + + cum_profits = create_cum_profit(df.set_index('date'), + bt_data[bt_data["pair"] == 'POWR/BTC'], + "cum_profits") + + fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits') + figure = fig1.layout.figure + profits = find_trace_in_fig_data(figure.data, "Profits") + assert isinstance(profits, go.Scattergl) + assert profits.yaxis == "y2" From 0b517584aacc12d9c1b054b77c29a52633c9b3b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:25:48 +0200 Subject: [PATCH 049/191] Use add_profit in script --- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/test_plotting.py | 12 ++++++------ scripts/plot_profit.py | 18 +++--------------- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f8736fe74..4c45c0375 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -7,7 +7,7 @@ import pandas as pd from freqtrade.arguments import Arguments from freqtrade.exchange import Exchange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades, create_cum_profit +from freqtrade.data.btanalysis import load_trades from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 2a73ad24e..fb2c52e1e 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,13 +5,13 @@ from unittest.mock import MagicMock import plotly.graph_objs as go from plotly import tools -from freqtrade.arguments import TimeRange, Arguments +from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import load_backtest_data, create_cum_profit -from freqtrade.plot.plotting import (generate_candlestick_graph, - store_plot_file, add_profit, - generate_plot_filename, add_indicators, - plot_trades) +from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data +from freqtrade.plot.plotting import (add_indicators, add_profit, + generate_candlestick_graph, + generate_plot_filename, plot_trades, + store_plot_file) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 248eeb7b0..ad135b5e6 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -8,14 +8,13 @@ import logging import sys from typing import Any, Dict, List -import pandas as pd import plotly.graph_objs as go from plotly import tools from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments from freqtrade.data.btanalysis import create_cum_profit, combine_tickers_with_mean from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import FTPlots, store_plot_file +from freqtrade.plot.plotting import FTPlots, store_plot_file, add_profit from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -48,27 +47,16 @@ def plot_profit(config: Dict[str, Any]) -> None: name='Avg close price', ) - profit = go.Scattergl( - x=df_comb.index, - y=df_comb['cum_profit'], - name='Profit', - ) - fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) fig.append_trace(avgclose, 1, 1) - fig.append_trace(profit, 2, 1) + fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') for pair in plot.pairs: profit_col = f'cum_profit_{pair}' df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) - pair_profit = go.Scattergl( - x=df_comb.index, - y=df_comb[profit_col], - name=f"Profit {pair}", - ) - fig.append_trace(pair_profit, 3, 1) + fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) From c7a4a16eec3778ccc6b8b2cdab66e0c3434ad242 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:31:36 +0200 Subject: [PATCH 050/191] Create generate_plot_graph --- freqtrade/plot/plotting.py | 35 +++++++++++++++++++++++++++++++++-- scripts/plot_profit.py | 33 ++------------------------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 4c45c0375..04e246371 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -5,9 +5,10 @@ from typing import Any, Dict, List, Optional import pandas as pd from freqtrade.arguments import Arguments -from freqtrade.exchange import Exchange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades +from freqtrade.data.btanalysis import (combine_tickers_with_mean, + create_cum_profit, load_trades) +from freqtrade.exchange import Exchange from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) @@ -28,6 +29,7 @@ class FTPlots(): self._config = config self.exchange: Optional[Exchange] = None + # Exchange is only needed when downloading data! if self._config.get("live", False) or self._config.get("refresh_pairs", False): self.exchange = ExchangeResolver(self._config.get('exchange', {}).get('name'), self._config).exchange @@ -258,6 +260,35 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra return fig +def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: pd.DataFrame = None, + ) -> go.Figure: + # Combine close-values for all pairs, rename columns to "pair" + df_comb = combine_tickers_with_mean(tickers, "close") + + # Add combined cumulative profit + df_comb = create_cum_profit(df_comb, trades, 'cum_profit') + + # Plot the pairs average close prices, and total profit growth + avgclose = go.Scattergl( + x=df_comb.index, + y=df_comb['mean'], + name='Avg close price', + ) + + fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) + + fig.append_trace(avgclose, 1, 1) + fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') + + for pair in pairs: + profit_col = f'cum_profit_{pair}' + df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) + + fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") + + store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) + + def generate_plot_filename(pair, ticker_interval) -> str: """ Generate filenames per pair/ticker_interval to be used for storing plots diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index ad135b5e6..f1cf99828 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -8,13 +8,9 @@ import logging import sys from typing import Any, Dict, List -import plotly.graph_objs as go -from plotly import tools - from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments -from freqtrade.data.btanalysis import create_cum_profit, combine_tickers_with_mean from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import FTPlots, store_plot_file, add_profit +from freqtrade.plot.plotting import FTPlots, generate_profit_graph from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -33,32 +29,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - - # Combine close-values for all pairs, rename columns to "pair" - df_comb = combine_tickers_with_mean(plot.tickers, "close") - - # Add combined cumulative profit - df_comb = create_cum_profit(df_comb, trades, 'cum_profit') - - # Plot the pairs average close prices, and total profit growth - avgclose = go.Scattergl( - x=df_comb.index, - y=df_comb['mean'], - name='Avg close price', - ) - - fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) - - fig.append_trace(avgclose, 1, 1) - fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') - - for pair in plot.pairs: - profit_col = f'cum_profit_{pair}' - df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) - - fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") - - store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) + generate_profit_graph(plot.pairs, plot.tickers, trades) def plot_parse_args(args: List[str]) -> Dict[str, Any]: From 587d71efb586b336e5c7ed50feb83b5f1f70afbd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:47:55 +0200 Subject: [PATCH 051/191] Test generate_profit_plot --- freqtrade/plot/plotting.py | 3 ++- freqtrade/tests/test_plotting.py | 34 +++++++++++++++++++++++++++++++- scripts/plot_profit.py | 5 +++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 04e246371..922f2847f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -276,6 +276,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: ) fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) + fig['layout'].update(title="Profit plot") fig.append_trace(avgclose, 1, 1) fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') @@ -286,7 +287,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") - store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) + return fig def generate_plot_filename(pair, ticker_interval) -> str: diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index fb2c52e1e..32e7dcd8a 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -10,7 +10,8 @@ from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.plot.plotting import (add_indicators, add_profit, generate_candlestick_graph, - generate_plot_filename, plot_trades, + generate_plot_filename, + generate_profit_graph, plot_trades, store_plot_file) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re @@ -214,3 +215,34 @@ def test_add_profit(): profits = find_trace_in_fig_data(figure.data, "Profits") assert isinstance(profits, go.Scattergl) assert profits.yaxis == "y2" + + +def test_generate_profit_graph(): + filename = history.make_testdata_path(None) / "backtest-result_test.json" + trades = load_backtest_data(filename) + timerange = Arguments.parse_timerange("20180110-20180112") + pairs = ["POWR/BTC", "XLM/BTC"] + + tickers = history.load_data(datadir=None, + pairs=pairs, + ticker_interval='5m', + timerange=timerange + ) + trades = trades[trades['pair'].isin(pairs)] + + fig = generate_profit_graph(pairs, tickers, trades) + assert isinstance(fig, go.Figure) + + assert fig.layout.title.text == "Profit plot" + figure = fig.layout.figure + assert len(figure.data) == 4 + + avgclose = find_trace_in_fig_data(figure.data, "Avg close price") + assert isinstance(avgclose, go.Scattergl) + + profit = find_trace_in_fig_data(figure.data, "Profit") + assert isinstance(profit, go.Scattergl) + + for pair in pairs: + profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}") + assert isinstance(profit_pair, go.Scattergl) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index f1cf99828..c29b4d967 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import FTPlots, generate_profit_graph +from freqtrade.plot.plotting import FTPlots, generate_profit_graph, store_plot_file from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -29,7 +29,8 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - generate_profit_graph(plot.pairs, plot.tickers, trades) + fig = generate_profit_graph(plot.pairs, plot.tickers, trades) + store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) def plot_parse_args(args: List[str]) -> Dict[str, Any]: From db59d39e2c40a96c54e2e1abe5179765a98d3883 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 11:06:51 +0200 Subject: [PATCH 052/191] Don't use class for plotting This will allow easy usage of the methods from jupter notebooks --- freqtrade/plot/plotting.py | 60 +++++++++++++++++++++----------------- scripts/plot_dataframe.py | 11 +++---- scripts/plot_profit.py | 11 +++---- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 922f2847f..079a098dc 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional import pandas as pd @@ -23,37 +23,43 @@ except ImportError: exit(1) -class FTPlots(): +def init_plotscript(config): + """ + Initialize objects needed for plotting + :return: Dict with tickers, trades, pairs and strategy + """ + exchange: Optional[Exchange] = None - def __init__(self, config: Dict[str, Any]): - self._config = config - self.exchange: Optional[Exchange] = None + # Exchange is only needed when downloading data! + if config.get("live", False) or config.get("refresh_pairs", False): + exchange = ExchangeResolver(config.get('exchange', {}).get('name'), + config).exchange - # Exchange is only needed when downloading data! - if self._config.get("live", False) or self._config.get("refresh_pairs", False): - self.exchange = ExchangeResolver(self._config.get('exchange', {}).get('name'), - self._config).exchange + strategy = StrategyResolver(config).strategy + if "pairs" in config: + pairs = config["pairs"].split(',') + else: + pairs = config["exchange"]["pair_whitelist"] - self.strategy = StrategyResolver(self._config).strategy - if "pairs" in self._config: - self.pairs = self._config["pairs"].split(',') - else: - self.pairs = self._config["exchange"]["pair_whitelist"] + # Set timerange to use + timerange = Arguments.parse_timerange(config["timerange"]) - # Set timerange to use - self.timerange = Arguments.parse_timerange(self._config["timerange"]) + 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), + ) - self.tickers = history.load_data( - datadir=Path(str(self._config.get("datadir"))), - pairs=self.pairs, - ticker_interval=self._config['ticker_interval'], - refresh_pairs=self._config.get('refresh_pairs', False), - timerange=self.timerange, - exchange=self.exchange, - live=self._config.get("live", False), - ) - - self.trades = load_trades(self._config) + trades = load_trades(config) + return {"tickers": tickers, + "trades": trades, + "pairs": pairs, + "strategy": strategy, + } def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 431c6239c..1e2d9f248 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -21,7 +21,7 @@ import pandas as pd from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments from freqtrade.data.btanalysis import extract_trades_of_period from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import (FTPlots, generate_candlestick_graph, +from freqtrade.plot.plotting import (init_plotscript, generate_candlestick_graph, store_plot_file, generate_plot_filename) from freqtrade.state import RunMode @@ -54,17 +54,18 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): -Generate plot files :return: None """ - plot = FTPlots(config) + plot_elements = init_plotscript(config) + trades = plot_elements['trades'] pair_counter = 0 - for pair, data in plot.tickers.items(): + for pair, data in plot_elements["tickers"].items(): pair_counter += 1 logger.info("analyse pair %s", pair) tickers = {} tickers[pair] = data - dataframe = generate_dataframe(plot.strategy, tickers, pair) + dataframe = generate_dataframe(plot_elements["strategy"], tickers, pair) - trades_pair = plot.trades.loc[plot.trades['pair'] == pair] + trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) fig = generate_candlestick_graph( diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index c29b4d967..7442ef155 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import FTPlots, generate_profit_graph, store_plot_file +from freqtrade.plot.plotting import init_plotscript, generate_profit_graph, store_plot_file from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -23,13 +23,14 @@ def plot_profit(config: Dict[str, Any]) -> None: But should be somewhat proportional, and therefor useful in helping out to find a good algorithm. """ - plot = FTPlots(config) - - trades = plot.trades[plot.trades['pair'].isin(plot.pairs)] + plot_elements = init_plotscript(config) + trades = plot_elements['trades'] + # Filter trades to relevant pairs + trades = trades[trades['pair'].isin(plot_elements["pairs"])] # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - fig = generate_profit_graph(plot.pairs, plot.tickers, trades) + fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades) store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) From 44e050095859004c0c2a913027cc5eee01d71a08 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 13:01:12 +0200 Subject: [PATCH 053/191] Test init_plotscript --- freqtrade/tests/test_plotting.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 32e7dcd8a..cef229d19 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -11,8 +11,8 @@ from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.plot.plotting import (add_indicators, add_profit, generate_candlestick_graph, generate_plot_filename, - generate_profit_graph, plot_trades, - store_plot_file) + generate_profit_graph, init_plotscript, + plot_trades, store_plot_file) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re @@ -37,6 +37,26 @@ def generage_empty_figure(): ) +def test_init_plotscript(default_conf, mocker): + default_conf['timerange'] = "20180110-20180112" + default_conf['trade_source'] = "file" + default_conf['ticker_interval'] = "5m" + default_conf["datadir"] = history.make_testdata_path(None) + default_conf['exportfilename'] = str( + history.make_testdata_path(None) / "backtest-result_test.json") + ret = init_plotscript(default_conf) + assert "tickers" in ret + assert "trades" in ret + assert "pairs" in ret + assert "strategy" in ret + + default_conf['pairs'] = "POWR/BTC,XLM/BTC" + ret = init_plotscript(default_conf) + assert "tickers" in ret + assert "POWR/BTC" in ret["tickers"] + assert "XLM/BTC" in ret["tickers"] + + def test_add_indicators(default_conf, caplog): pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) From 59818af69ced404503ab543c4ce03752c6016082 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 13:15:41 +0200 Subject: [PATCH 054/191] Remove common_datearray function --- freqtrade/misc.py | 20 -------------------- freqtrade/plot/plotting.py | 4 ++-- freqtrade/tests/test_misc.py | 21 +++------------------ 3 files changed, 5 insertions(+), 40 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 460e20e91..05946e008 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -5,10 +5,8 @@ import gzip import logging import re from datetime import datetime -from typing import Dict import numpy as np -from pandas import DataFrame import rapidjson @@ -41,24 +39,6 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: return dates.dt.to_pydatetime() -def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray: - """ - Return dates from Dataframe - :param dfs: Dict with format pair: pair_data - :return: List of dates - """ - alldates = {} - for pair, pair_data in dfs.items(): - dates = datesarray_to_datetimearray(pair_data['date']) - for date in dates: - alldates[date] = 1 - lst = [] - for date, _ in alldates.items(): - lst.append(date) - arr = np.array(lst) - return np.sort(arr, axis=0) - - def file_dump_json(filename, data, is_zip=False) -> None: """ Dump JSON data into a file diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 079a098dc..ccb932698 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -266,8 +266,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra return fig -def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: pd.DataFrame = None, - ) -> go.Figure: +def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], + trades: pd.DataFrame) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" df_comb = combine_tickers_with_mean(tickers, "close") diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 7a7b15cf2..1a6b2a92d 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -4,10 +4,9 @@ import datetime from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe -from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, - file_dump_json, file_load_json, format_ms_time, shorten_date) -from freqtrade.data.history import load_tickerdata_file, pair_data_filename -from freqtrade.strategy.default_strategy import DefaultStrategy +from freqtrade.data.history import pair_data_filename +from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, + file_load_json, format_ms_time, shorten_date) def test_shorten_date() -> None: @@ -32,20 +31,6 @@ def test_datesarray_to_datetimearray(ticker_history_list): assert date_len == 2 -def test_common_datearray(default_conf) -> None: - strategy = DefaultStrategy(default_conf) - tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') - tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", pair="UNITTEST/BTC", - fill_missing=True)} - dataframes = strategy.tickerdata_to_dataframe(tickerlist) - - dates = common_datearray(dataframes) - - assert dates.size == dataframes['UNITTEST/BTC']['date'].size - assert dates[0] == dataframes['UNITTEST/BTC']['date'][0] - assert dates[-1] == dataframes['UNITTEST/BTC']['date'].iloc[-1] - - def test_file_dump_json(mocker) -> None: file_open = mocker.patch('freqtrade.misc.open', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock()) From 0c7d14fe50694edd916c550b1c1213990146f648 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 20:30:31 +0200 Subject: [PATCH 055/191] Check if timeframes is available and fail gracefully otherwise --- freqtrade/exchange/exchange.py | 7 +++++++ freqtrade/resolvers/exchange_resolver.py | 1 + 2 files changed, 8 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b649a7b65..cefee999c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -269,6 +269,13 @@ class Exchange(object): """ Checks if ticker interval from config is a supported timeframe on the exchange """ + logger.warning("validate_timerames") + if not hasattr(self._api, "timeframes"): + # If timeframes is missing, the exchange probably has no fetchOHLCV method. + # Therefore we also show that. + raise OperationalException( + f"This exchange ({self.name}) does not have a `timeframes` attribute and " + f"is therefore not supported. fetchOHLCV: {self.exchange_has('fetchOHLCV')}") timeframes = self._api.timeframes if timeframe not in timeframes: raise OperationalException( diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 25a86dd0e..089f6306f 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -28,6 +28,7 @@ class ExchangeResolver(IResolver): except ImportError: logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") + if not hasattr(self, "exchange"): self.exchange = Exchange(config) def _load_exchange( From 01904d3c1e47321140304faecb768a3709bc751c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 20:30:47 +0200 Subject: [PATCH 056/191] Test not having timeframe available on exchange object --- freqtrade/tests/exchange/test_exchange.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 48a8538a9..b74882ad4 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -396,6 +396,23 @@ def test_validate_timeframes_failed(default_conf, mocker): Exchange(default_conf) +def test_validate_timeframes_emulated_ohlcv(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + # delete timeframes so magicmock does not autocreate it + del api_mock.timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + with pytest.raises(OperationalException, + match=r'This exchange (.*) does not have a `timeframes` attribute and*'): + Exchange(default_conf) + + def test_validate_timeframes_not_in_config(default_conf, mocker): del default_conf["ticker_interval"] api_mock = MagicMock() From 0d601fd1113615d1155986ffe65ba9b49067b68c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jul 2019 06:18:28 +0200 Subject: [PATCH 057/191] Remove logger message --- freqtrade/exchange/exchange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cefee999c..2350c752f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -269,7 +269,6 @@ class Exchange(object): """ Checks if ticker interval from config is a supported timeframe on the exchange """ - logger.warning("validate_timerames") if not hasattr(self._api, "timeframes"): # If timeframes is missing, the exchange probably has no fetchOHLCV method. # Therefore we also show that. From 06ad04e5fa8f48adac1d1ae12caf5689464569f3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jul 2019 18:28:30 +0000 Subject: [PATCH 058/191] Update ccxt from 1.18.805 to 1.18.860 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 6913217ff..409c979b5 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.805 +ccxt==1.18.860 SQLAlchemy==1.3.5 python-telegram-bot==11.1.0 arrow==0.14.2 From 1e4f459a2678e377e418480b5415d6c59c87b13b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jul 2019 18:28:31 +0000 Subject: [PATCH 059/191] Update pytest from 4.6.3 to 5.0.0 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1232d4dd4..99697340d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.6.3 +pytest==5.0.0 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From c91add203d451b93d384af919f99d5d4edc408e8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jul 2019 18:28:32 +0000 Subject: [PATCH 060/191] Update mypy from 0.710 to 0.711 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 99697340d..c360bc85e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,4 +10,4 @@ pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 coveralls==1.8.1 -mypy==0.710 +mypy==0.711 From 85ac217abc5303fb2be26a1dfd1ec2bc0e14d433 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jul 2019 20:33:27 +0200 Subject: [PATCH 061/191] Remove duplicate keyword from arguments --- freqtrade/arguments.py | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f020252f8..f699cb549 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -36,13 +36,11 @@ AVAILABLE_CLI_OPTIONS = { '-v', '--verbose', help='Verbose mode (-vv for more, -vvv to get all messages).', action='count', - dest='loglevel', default=0, ), "logfile": Arg( '--logfile', help='Log to the file specified.', - dest='logfile', metavar='FILE', ), @@ -56,26 +54,22 @@ AVAILABLE_CLI_OPTIONS = { 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',), "datadir": Arg( '-d', '--datadir', help='Path to backtest data.', - dest='datadir', metavar='PATH',), # Main options "strategy": Arg( '-s', '--strategy', help='Specify strategy class name (default: `%(default)s`).', - dest='strategy', default='DefaultStrategy', metavar='NAME', ), "strategy_path": Arg( '--strategy-path', help='Specify additional strategy lookup path.', - dest='strategy_path', metavar='PATH', ), "dynamic_whitelist": Arg( @@ -83,7 +77,6 @@ AVAILABLE_CLI_OPTIONS = { help='Dynamically generate and update whitelist ' 'based on 24h BaseVolume (default: %(const)s). ' 'DEPRECATED.', - dest='dynamic_whitelist', const=constants.DYNAMIC_WHITELIST, type=int, metavar='INT', @@ -94,37 +87,31 @@ AVAILABLE_CLI_OPTIONS = { help=f'Override trades database URL, this is useful in custom deployments ' f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', - dest='db_url', metavar='PATH', ), "sd_notify": Arg( '--sd-notify', help='Notify systemd service manager.', action='store_true', - dest='sd_notify', ), # Optimize common "ticker_interval": Arg( '-i', '--ticker-interval', help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', - dest='ticker_interval', ), "timerange": Arg( '--timerange', help='Specify what timerange of data to use.', - dest='timerange', ), "max_open_trades": Arg( '--max_open_trades', help='Specify max_open_trades to use.', type=int, - dest='max_open_trades', ), "stake_amount": Arg( '--stake_amount', help='Specify stake_amount.', type=float, - dest='stake_amount', ), "refresh_pairs": Arg( '-r', '--refresh-pairs-cached', @@ -132,14 +119,12 @@ AVAILABLE_CLI_OPTIONS = { 'exchange. Use it if you want to run your optimization commands with ' 'up-to-date data.', action='store_true', - dest='refresh_pairs', ), # backtesting "position_stacking": Arg( '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', action='store_true', - dest='position_stacking', default=False ), "use_max_market_positions": Arg( @@ -147,14 +132,12 @@ AVAILABLE_CLI_OPTIONS = { help='Disable applying `max_open_trades` during backtest ' '(same as setting `max_open_trades` to a very high number).', action='store_false', - dest='use_max_market_positions', default=True ), "live": Arg( '-l', '--live', help='Use live data.', action='store_true', - dest='live', ), "strategy_list": Arg( '--strategy-list', @@ -164,13 +147,11 @@ AVAILABLE_CLI_OPTIONS = { 'the strategy-name is injected into the filename ' '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', nargs='+', - dest='strategy_list', ), "export": Arg( '--export', help='Export backtest results, argument are: trades. ' 'Example: `--export=trades`', - dest='export', ), "exportfilename": Arg( '--export-filename', @@ -179,7 +160,6 @@ AVAILABLE_CLI_OPTIONS = { 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), - dest='exportfilename', metavar='PATH', ), # Edge @@ -188,20 +168,17 @@ AVAILABLE_CLI_OPTIONS = { help='Defines a range of stoploss values against which edge will assess the strategy. ' 'The format is "min,max,step" (without any space). ' 'Example: `--stoplosses=-0.01,-0.1,-0.001`', - dest='stoploss_range', ), # hyperopt "hyperopt": Arg( '--customhyperopt', help='Specify hyperopt class name (default: `%(default)s`).', - dest='hyperopt', default=constants.DEFAULT_HYPEROPT, metavar='NAME', ), "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', - dest='epochs', default=constants.HYPEROPT_EPOCH, type=int, metavar='INT', @@ -213,13 +190,11 @@ AVAILABLE_CLI_OPTIONS = { choices=['all', 'buy', 'sell', 'roi', 'stoploss'], default='all', nargs='+', - dest='spaces', ), "print_all": Arg( '--print-all', help='Print all results, not only the best ones.', action='store_true', - dest='print_all', default=False ), "hyperopt_jobs": Arg( @@ -228,7 +203,6 @@ AVAILABLE_CLI_OPTIONS = { '(hyperopt worker processes). ' 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' 'If 1 is given, no parallel computing code is used at all.', - dest='hyperopt_jobs', default=-1, type=int, metavar='JOBS', @@ -236,7 +210,6 @@ AVAILABLE_CLI_OPTIONS = { "hyperopt_random_state": Arg( '--random-state', help='Set random state to some positive integer for reproducible hyperopt results.', - dest='hyperopt_random_state', type=check_int_positive, metavar='INT', ), @@ -244,7 +217,6 @@ AVAILABLE_CLI_OPTIONS = { '--min-trades', help="Set minimal desired number of trades for evaluations in the hyperopt " "optimization path (default: 1).", - dest='hyperopt_min_trades', default=1, type=check_int_positive, metavar='INT', @@ -254,26 +226,22 @@ AVAILABLE_CLI_OPTIONS = { '-1', '--one-column', help='Print exchanges in one column.', action='store_true', - dest='print_one_column', ), # script_options "pairs": Arg( '-p', '--pairs', help='Show profits for only these pairs. Pairs are comma-separated.', - dest='pairs', ), # Download data "pairs_file": Arg( '--pairs-file', help='File containing a list of pairs to download.', - dest='pairs_file', metavar='FILE', ), "days": Arg( '--days', help='Download data for given number of days.', - dest='days', type=check_int_positive, metavar='INT', ), @@ -281,7 +249,6 @@ AVAILABLE_CLI_OPTIONS = { '--exchange', help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' f'Only valid if no config is provided.', - dest='exchange', ), "timeframes": Arg( '-t', '--timeframes', @@ -290,12 +257,10 @@ AVAILABLE_CLI_OPTIONS = { choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], nargs='+', - dest='timeframes', ), "erase": Arg( '--erase', help='Clean all existing data for the selected exchange/pairs/timeframes.', - dest='erase', action='store_true', ), # Plot_df_options @@ -304,20 +269,17 @@ AVAILABLE_CLI_OPTIONS = { help='Set indicators from your strategy you want in the first row of the graph. ' 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', default='sma,ema3,ema5', - dest='indicators1', ), "indicators2": Arg( '--indicators2', help='Set indicators from your strategy you want in the third row of the graph. ' 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', default='macd,macdsignal', - dest='indicators2', ), "plot_limit": Arg( '--plot-limit', help='Specify tick limit for plotting. Notice: too high values cause huge files. ' 'Default: %(default)s.', - dest='plot_limit', default=750, type=int, ), @@ -325,7 +287,6 @@ AVAILABLE_CLI_OPTIONS = { '--trade-source', help='Specify the source for trades (Can be DB or file (backtest file)) ' 'Default: %(default)s', - dest='trade_source', default="file", choices=["DB", "file"] ) @@ -418,7 +379,7 @@ class Arguments(object): for val in optionlist: opt = AVAILABLE_CLI_OPTIONS[val] - parser.add_argument(*opt.cli, **opt.kwargs) + parser.add_argument(*opt.cli, dest=val, **opt.kwargs) def _build_subcommands(self) -> None: """ From 91fb9d0113d719d4e6d884e93ec7bc15017b4ccc Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 3 Jul 2019 05:02:44 +0300 Subject: [PATCH 062/191] fix #1995 --- freqtrade/exchange/exchange.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2350c752f..556ede13a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -85,6 +85,9 @@ class Exchange(object): it does basic validation whether the specified exchange and pairs are valid. :return: None """ + self._api: ccxt.Exchange = None + self._api_async: ccxt_async.Exchange = None + self._config.update(config) self._cached_ticker: Dict[str, Any] = {} @@ -117,9 +120,9 @@ class Exchange(object): self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] # Initialize ccxt objects - self._api: ccxt.Exchange = self._init_ccxt( + self._api = self._init_ccxt( exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) - self._api_async: ccxt_async.Exchange = self._init_ccxt( + self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config')) logger.info('Using Exchange "%s"', self.name) @@ -173,6 +176,8 @@ class Exchange(object): api = getattr(ccxt_module, name.lower())(ex_config) except (KeyError, AttributeError): raise OperationalException(f'Exchange {name} is not supported') + except ccxt.NotSupported as e: + raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") self.set_sandbox(api, exchange_config, name) From d41b8cc96ec56b372889ab5ee213311586cb8c47 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 3 Jul 2019 05:13:41 +0300 Subject: [PATCH 063/191] catch ccxt.BaseError --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 556ede13a..c3171b961 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -176,7 +176,7 @@ class Exchange(object): api = getattr(ccxt_module, name.lower())(ex_config) except (KeyError, AttributeError): raise OperationalException(f'Exchange {name} is not supported') - except ccxt.NotSupported as e: + except ccxt.BaseError as e: raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") self.set_sandbox(api, exchange_config, name) From b3644f7fa000e2be4daabceab47216175552a1ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2019 06:26:39 +0200 Subject: [PATCH 064/191] Fix typo in docstring --- freqtrade/data/btanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 556666f4e..dcd544d00 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -106,7 +106,7 @@ def load_trades(config) -> pd.DataFrame: """ Based on configuration option "trade_source": * loads data from DB (using `db_url`) - * loads data from backtestfile (`using exportfilename`) + * loads data from backtestfile (using `exportfilename`) """ if config["trade_source"] == "DB": return load_trades_from_db(config["db_url"]) From b80cef964e5461fd19a7329982d97402e2ca3333 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 3 Jul 2019 01:03:38 +0300 Subject: [PATCH 065/191] fix validate_timeframes(); test added --- freqtrade/exchange/exchange.py | 11 ++++++---- freqtrade/tests/exchange/test_exchange.py | 26 +++++++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2350c752f..71055460b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -269,12 +269,15 @@ class Exchange(object): """ Checks if ticker interval from config is a supported timeframe on the exchange """ - if not hasattr(self._api, "timeframes"): - # If timeframes is missing, the exchange probably has no fetchOHLCV method. + if not hasattr(self._api, "timeframes") or self._api.timeframes is None: + # If timeframes attribute is missing (or is None), the exchange probably + # has no fetchOHLCV method. # Therefore we also show that. raise OperationalException( - f"This exchange ({self.name}) does not have a `timeframes` attribute and " - f"is therefore not supported. fetchOHLCV: {self.exchange_has('fetchOHLCV')}") + f"The ccxt library does not provide the list of timeframes " + f"for the exchange \"{self.name}\" and this exchange " + f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}") + timeframes = self._api.timeframes if timeframe not in timeframes: raise OperationalException( diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index b74882ad4..1d4b0bb11 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -396,7 +396,7 @@ def test_validate_timeframes_failed(default_conf, mocker): Exchange(default_conf) -def test_validate_timeframes_emulated_ohlcv(default_conf, mocker): +def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): default_conf["ticker_interval"] = "3m" api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') @@ -409,7 +409,29 @@ def test_validate_timeframes_emulated_ohlcv(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) with pytest.raises(OperationalException, - match=r'This exchange (.*) does not have a `timeframes` attribute and*'): + match=r'The ccxt library does not provide the list of timeframes ' + r'for the exchange ".*" and this exchange ' + r'is therefore not supported. *'): + Exchange(default_conf) + + +def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + # delete timeframes so magicmock does not autocreate it + del api_mock.timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange._load_markets', + MagicMock(return_value={'timeframes': None})) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + with pytest.raises(OperationalException, + match=r'The ccxt library does not provide the list of timeframes ' + r'for the exchange ".*" and this exchange ' + r'is therefore not supported. *'): Exchange(default_conf) From fcdbe846e51a9c76f86c97b84dec28680d857a71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2019 20:06:50 +0200 Subject: [PATCH 066/191] Fix #1981 - Detect reverted currency pairs --- freqtrade/exchange/exchange.py | 11 ++++++++++- freqtrade/rpc/rpc.py | 7 ++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c3171b961..536a707cd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -270,6 +270,15 @@ class Exchange(object): f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') + def get_valid_pair_combination(self, paira, pairb) -> str: + """ + Get valid combination of paira and pairb by trying both combinations. + """ + for pair in [f"{paira}/{pairb}", f"{pairb}/{paira}"]: + if pair in self._api.markets and self._api.markets[pair].get('active'): + return pair + raise DependencyException(f"Could not combine {paira} and {pairb} to get a valid pair.") + def validate_timeframes(self, timeframe: List[str]) -> None: """ Checks if ticker interval from config is a supported timeframe on the exchange @@ -501,7 +510,7 @@ class Exchange(object): def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: if refresh or pair not in self._cached_ticker.keys(): try: - if pair not in self._api.markets: + if pair not in self._api.markets or not self._api.markets[pair].get('active'): raise DependencyException(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) try: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f5adffc65..f77e0eddb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -281,10 +281,11 @@ class RPC(object): rate = 1.0 else: try: - if coin in('USDT', 'USD', 'EUR'): - rate = 1.0 / self._freqtrade.get_sell_rate('BTC/' + coin, False) + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC") + if pair.startswith("BTC"): + rate = 1.0 / self._freqtrade.get_sell_rate(pair, False) else: - rate = self._freqtrade.get_sell_rate(coin + '/BTC', False) + rate = self._freqtrade.get_sell_rate(pair, False) except (TemporaryError, DependencyException): logger.warning(f" Could not get rate for pair {coin}.") continue From 1bcf2737fe871701516d15ba120037ba51f93b1b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2019 20:07:26 +0200 Subject: [PATCH 067/191] Add tests for new behaviour --- freqtrade/tests/exchange/test_exchange.py | 2 +- freqtrade/tests/rpc/test_rpc.py | 68 ++++++++++++++++++++++- freqtrade/tests/rpc/test_rpc_apiserver.py | 2 + freqtrade/tests/rpc/test_rpc_telegram.py | 2 + 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index b74882ad4..804cf1af5 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -910,7 +910,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): 'last': 0.0001, } api_mock.fetch_ticker = MagicMock(return_value=tick) - api_mock.markets = {'ETH/BTC': {}} + api_mock.markets = {'ETH/BTC': {'active': True}} exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker ticker = exchange.get_ticker(pair='ETH/BTC') diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 5a4b5d1b2..d273244b0 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -324,7 +324,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, assert prec_satoshi(stats['best_rate'], 6.2) -def test_rpc_balance_handle(default_conf, mocker): +def test_rpc_balance_handle_error(default_conf, mocker): mock_balance = { 'BTC': { 'free': 10.0, @@ -371,6 +371,72 @@ def test_rpc_balance_handle(default_conf, mocker): assert result['total'] == 12.0 +def test_rpc_balance_handle(default_conf, mocker): + mock_balance = { + 'BTC': { + 'free': 10.0, + 'total': 12.0, + 'used': 2.0, + }, + 'ETH': { + 'free': 1.0, + 'total': 5.0, + 'used': 4.0, + }, + 'PAX': { + 'free': 5.0, + 'total': 10.0, + 'used': 5.0, + } + } + + mocker.patch.multiple( + 'freqtrade.rpc.fiat_convert.Market', + ticker=MagicMock(return_value={'price_usd': 15000.0}), + ) + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=mock_balance), + get_ticker=MagicMock( + side_effect=lambda p, r: {'bid': 100} if p == "BTC/PAX" else {'bid': 0.01}), + get_valid_pair_combination=MagicMock( + side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}") + ) + + freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) + rpc = RPC(freqtradebot) + rpc._fiat_converter = CryptoToFiatConverter() + + result = rpc._rpc_balance(default_conf['fiat_display_currency']) + assert prec_satoshi(result['total'], 12.15) + assert prec_satoshi(result['value'], 182250) + assert 'USD' == result['symbol'] + assert result['currencies'] == [ + {'currency': 'BTC', + 'available': 10.0, + 'balance': 12.0, + 'pending': 2.0, + 'est_btc': 12.0, + }, + {'available': 1.0, + 'balance': 5.0, + 'currency': 'ETH', + 'est_btc': 0.05, + 'pending': 4.0 + }, + {'available': 5.0, + 'balance': 10.0, + 'currency': 'PAX', + 'est_btc': 0.1, + 'pending': 5.0} + ] + assert result['total'] == 12.15 + + def test_rpc_start(mocker, default_conf) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index b7721fd8e..bd420ada6 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -244,6 +244,8 @@ def test_api_balance(botclient, mocker, rpc_balance): } mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', + side_effect=lambda a, b: f"{a}/{b}") rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 673536993..1bee5bff3 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -518,6 +518,8 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> N mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', + side_effect=lambda a, b: f"{a}/{b}") msg_mock = MagicMock() mocker.patch.multiple( From 40fe2d2c164bddd3ce6f17f84a2d636498636ff4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2019 20:20:12 +0200 Subject: [PATCH 068/191] Test get_valid_pair_combination --- freqtrade/exchange/exchange.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 24 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 536a707cd..a41b51e90 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -275,7 +275,7 @@ class Exchange(object): Get valid combination of paira and pairb by trying both combinations. """ for pair in [f"{paira}/{pairb}", f"{pairb}/{paira}"]: - if pair in self._api.markets and self._api.markets[pair].get('active'): + if pair in self.markets and self.markets[pair].get('active'): return pair raise DependencyException(f"Could not combine {paira} and {pairb} to get a valid pair.") diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 804cf1af5..4b264db08 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1455,10 +1455,11 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker): - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + validate_timeframes=MagicMock()) ex = Exchange(default_conf) assert ex._ft_has == Exchange._ft_has_default @@ -1479,3 +1480,18 @@ def test_merge_ft_has_dict(default_conf, mocker): assert ex._ft_has != Exchange._ft_has_default assert not ex._ft_has['stoploss_on_exchange'] assert ex._ft_has['DeadBeef'] == 20 + + +def test_get_valid_pair_combination(default_conf, mocker, markets): + mocker.patch.multiple('freqtrade.exchange.Exchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + validate_timeframes=MagicMock(), + markets=PropertyMock(return_value=markets)) + ex = Exchange(default_conf) + + assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC" + assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" + with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."): + ex.get_valid_pair_combination("NOPAIR", "ETH") From 5c6039fd8b6c70c32f712455aab0470ccfa46711 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2019 19:53:50 +0200 Subject: [PATCH 069/191] Fix #1997 - rename folder to dir --- freqtrade/configuration.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 8 ++++---- freqtrade/tests/optimize/test_edge_cli.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 4 ++-- freqtrade/tests/test_configuration.py | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 82349700e..2bbec4654 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -254,7 +254,7 @@ class Configuration(object): 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')) + logger.info('Using data directory: %s ...', config.get('datadir')) def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 28568f20c..498a04db5 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -183,7 +183,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -235,7 +235,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -851,7 +851,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', - 'Using data folder: freqtrade/tests/testdata ...', + 'Using data directory: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Live: Downloading data for all defined pairs ...', @@ -910,7 +910,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', - 'Using data folder: freqtrade/tests/testdata ...', + 'Using data directory: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Live: Downloading data for all defined pairs ...', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 6b527543f..7cc41b095 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -32,7 +32,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -71,7 +71,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N assert 'datadir' in config assert config['runmode'] == RunMode.EDGE assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index c3d6d0076..e5f87b022 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -61,7 +61,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -111,7 +111,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo assert config['runmode'] == RunMode.HYPEROPT assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 38f17fbea..b34e75a28 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -306,7 +306,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -356,7 +356,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -418,7 +418,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config From 17800c8ca561d48e5ad0da6484d148d63c76e54f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2019 19:56:48 +0200 Subject: [PATCH 070/191] Remove folder references (it's directory!) --- config_full.json.example | 2 +- docs/backtesting.md | 12 ++++++------ docs/bot-usage.md | 6 +++--- docs/configuration.md | 2 +- docs/docker.md | 2 +- docs/hyperopt.md | 2 +- docs/strategy-customization.md | 9 ++++----- freqtrade/tests/test_arguments.py | 4 ++-- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index acecfb649..b6451859c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -123,5 +123,5 @@ "process_throttle_secs": 5 }, "strategy": "DefaultStrategy", - "strategy_path": "/some/folder/" + "strategy_path": "user_data/strategies/" } diff --git a/docs/backtesting.md b/docs/backtesting.md index 8d8ea8030..2a5163e73 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -13,7 +13,7 @@ Backtesting will use the crypto-currencies (pair) from your config file and load static tickers located in [/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). If the 5 min and 1 min ticker for the crypto-currencies to test is not -already in the `testdata` folder, backtesting will download them +already in the `testdata` directory, backtesting will download them automatically. Testdata files will not be updated until you specify it. The result of backtesting will confirm you if your bot has better odds of making a profit than a loss. @@ -65,7 +65,7 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_ python3 freqtrade backtesting --export trades ``` -The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts folder. +The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. #### Exporting trades to file specifying a custom filename @@ -107,7 +107,7 @@ To download new set of backtesting ticker data, you can use a download script. If you are using Binance for example: -- create a folder `user_data/data/binance` and copy `pairs.json` in that folder. +- create a directory `user_data/data/binance` and copy `pairs.json` in that directory. - update the `pairs.json` to contain the currency pairs you are interested in. ```bash @@ -123,9 +123,9 @@ 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 `--datadir user_data/data/some_directory`. +- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. - To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`. -- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`. +- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To download ticker data for only 10 days, use `--days 10`. - Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. - To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options. @@ -231,7 +231,7 @@ To backtest multiple strategies, a list of Strategies can be provided. This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple strategies you'd like to compare, this should give a nice runtime boost. -All listed Strategies need to be in the same folder. +All listed Strategies need to be in the same directory. ``` bash freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades diff --git a/docs/bot-usage.md b/docs/bot-usage.md index b215d7b7c..0e01ac0e5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -109,14 +109,14 @@ Learn more about strategy file in ### How to use **--strategy-path**? This parameter allows you to add an additional strategy lookup path, which gets -checked before the default locations (The passed path must be a folder!): +checked before the default locations (The passed path must be a directory!): ```bash -python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder +python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` #### How to install a strategy? -This is very simple. Copy paste your strategy file into the folder +This is very simple. Copy paste your strategy file into the directory `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. ### How to use **--dynamic-whitelist**? diff --git a/docs/configuration.md b/docs/configuration.md index 9c3b20338..f46ff14a7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -67,7 +67,7 @@ Mandatory Parameters are marked as **Required**. | `initial_state` | running | Defines the initial application state. More information below. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. | `strategy` | DefaultStrategy | Defines Strategy class to use. -| `strategy_path` | null | Adds an additional strategy lookup path (must be a folder). +| `strategy_path` | null | Adds an additional strategy lookup path (must be a directory). | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. diff --git a/docs/docker.md b/docs/docker.md index 939ab3f7d..615d31796 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -140,7 +140,7 @@ To run a restartable instance in the background (feel free to place your configu #### Move your config file and database -The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands. +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. ```bash mkdir ~/.freqtrade diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 15b02b56f..a15fd575a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -34,7 +34,7 @@ Depending on the space you want to optimize, only some of the below are required ### 1. Install a Custom Hyperopt File -Put your hyperopt file into the folder`user_data/hyperopts`. +Put your hyperopt file into the directory `user_data/hyperopts`. Let assume you want a hyperopt file `awesome_hyperopt.py`: Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py` diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 57c646aed..800012a0f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -5,8 +5,7 @@ indicators. ## Install a custom strategy file -This is very simple. Copy paste your strategy file into the folder -`user_data/strategies`. +This is very simple. Copy paste your strategy file into the directory `user_data/strategies`. Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: @@ -22,7 +21,7 @@ python3 freqtrade --strategy AwesomeStrategy The bot includes a default strategy file. However, we recommend you to use your own file to not have to lose your parameters every time the default strategy file will be updated on Github. Put your custom strategy file -into the folder `user_data/strategies`. +into the directory `user_data/strategies`. Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. `cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py` @@ -398,10 +397,10 @@ The default buy strategy is located in the file ### Specify custom strategy location -If you want to use a strategy from a different folder you can pass `--strategy-path` +If you want to use a strategy from a different directory you can pass `--strategy-path` ```bash -python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder +python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` ### Further strategy ideas diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index c9d2a2261..8f0dec226 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -174,7 +174,7 @@ def test_parse_args_hyperopt_custom() -> None: def test_download_data_options() -> None: args = [ '--pairs-file', 'file_with_pairs', - '--datadir', 'datadir/folder', + '--datadir', 'datadir/directory', '--days', '30', '--exchange', 'binance' ] @@ -183,7 +183,7 @@ def test_download_data_options() -> None: args = arguments.parse_args() assert args.pairs_file == 'file_with_pairs' - assert args.datadir == 'datadir/folder' + assert args.datadir == 'datadir/directory' assert args.days == 30 assert args.exchange == 'binance' From ce2a5b28380a9c9757f1674ec2f71b1195406d29 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 00:31:48 +0300 Subject: [PATCH 071/191] move loggers setup out of configuration --- freqtrade/configuration.py | 34 ++++----------------------- freqtrade/main.py | 3 --- freqtrade/tests/test_configuration.py | 11 +++++---- 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 2bbec4654..f8488c438 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -6,8 +6,7 @@ import logging import os import sys from argparse import Namespace -from logging.handlers import RotatingFileHandler -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, Optional from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match @@ -15,25 +14,14 @@ from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants from freqtrade.exchange import (is_exchange_bad, is_exchange_available, is_exchange_officially_supported, available_exchanges) +from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode + logger = logging.getLogger(__name__) -def set_loggers(log_level: int = 0) -> None: - """ - Set the logger level for Third party libs - :return: None - """ - - logging.getLogger('requests').setLevel(logging.INFO if log_level <= 1 else logging.DEBUG) - logging.getLogger("urllib3").setLevel(logging.INFO if log_level <= 1 else logging.DEBUG) - logging.getLogger('ccxt.base.exchange').setLevel( - logging.INFO if log_level <= 2 else logging.DEBUG) - logging.getLogger('telegram').setLevel(logging.INFO) - - def _extend_validator(validator_class): """ Extended validator for the Freqtrade configuration JSON Schema. @@ -143,24 +131,10 @@ class Configuration(object): else: config.update({'verbosity': 0}) - # Log to stdout, not stderr - log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] if 'logfile' in self.args and self.args.logfile: config.update({'logfile': self.args.logfile}) - # Allow setting this as either configuration or argument - if 'logfile' in config: - log_handlers.append(RotatingFileHandler(config['logfile'], - maxBytes=1024 * 1024, # 1Mb - backupCount=10)) - - logging.basicConfig( - level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=log_handlers - ) - set_loggers(config['verbosity']) - logger.info('Verbosity set to %s', config['verbosity']) + setup_logging(config) def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/freqtrade/main.py b/freqtrade/main.py index 6f073f5d4..f02159a0e 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -16,7 +16,6 @@ from typing import Any, List from freqtrade import OperationalException from freqtrade.arguments import Arguments -from freqtrade.configuration import set_loggers from freqtrade.worker import Worker @@ -32,8 +31,6 @@ def main(sysargv: List[str] = None) -> None: return_code: Any = 1 worker = None try: - set_loggers() - arguments = Arguments( sysargv, 'Free, open source crypto trading bot' diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index b34e75a28..c5e60be7f 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -12,8 +12,9 @@ from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration, set_loggers +from freqtrade.configuration import Configuration from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL +from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re @@ -524,7 +525,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf))) # Prevent setting loggers - mocker.patch('freqtrade.configuration.set_loggers', MagicMock) + mocker.patch('freqtrade.loggers._set_loggers', MagicMock) arglist = ['-vvv'] args = Arguments(arglist, '').get_parsed_arg() @@ -546,7 +547,7 @@ def test_set_loggers() -> None: previous_value2 = logging.getLogger('ccxt.base.exchange').level previous_value3 = logging.getLogger('telegram').level - set_loggers() + _set_loggers() value1 = logging.getLogger('requests').level assert previous_value1 is not value1 @@ -560,13 +561,13 @@ def test_set_loggers() -> None: assert previous_value3 is not value3 assert value3 is logging.INFO - set_loggers(log_level=2) + _set_loggers(verbosity=2) assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.INFO assert logging.getLogger('telegram').level is logging.INFO - set_loggers(log_level=3) + _set_loggers(verbosity=3) assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG From 8e272e5774f74c5125b29a9d2bb50579a8480cc8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 00:48:39 +0300 Subject: [PATCH 072/191] minor: cosmetics in arguments.py --- freqtrade/arguments.py | 47 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index ebdaf7c6e..b2d905a5a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -43,11 +43,10 @@ AVAILABLE_CLI_OPTIONS = { help='Log to the file specified.', metavar='FILE', ), - "version": Arg( '--version', action='version', - version=f'%(prog)s {__version__}' + version=f'%(prog)s {__version__}', ), "config": Arg( '-c', '--config', @@ -55,17 +54,19 @@ AVAILABLE_CLI_OPTIONS = { f'Multiple --config options may be used. ' f'Can be set to `-` to read config from stdin.', action='append', - metavar='PATH',), + metavar='PATH', + ), "datadir": Arg( '-d', '--datadir', help='Path to backtest data.', - metavar='PATH',), + metavar='PATH', + ), # Main options "strategy": Arg( '-s', '--strategy', help='Specify strategy class name (default: `%(default)s`).', - default='DefaultStrategy', metavar='NAME', + default='DefaultStrategy', ), "strategy_path": Arg( '--strategy-path', @@ -125,14 +126,14 @@ AVAILABLE_CLI_OPTIONS = { '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', action='store_true', - default=False + default=False, ), "use_max_market_positions": Arg( '--dmmp', '--disable-max-market-positions', help='Disable applying `max_open_trades` during backtest ' '(same as setting `max_open_trades` to a very high number).', action='store_false', - default=True + default=True, ), "live": Arg( '-l', '--live', @@ -158,9 +159,9 @@ AVAILABLE_CLI_OPTIONS = { help='Save backtest results to the file with this filename (default: `%(default)s`). ' 'Requires `--export` to be set as well. ' 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', + metavar='PATH', default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), - metavar='PATH', ), # Edge "stoploss_range": Arg( @@ -169,33 +170,33 @@ AVAILABLE_CLI_OPTIONS = { 'The format is "min,max,step" (without any space). ' 'Example: `--stoplosses=-0.01,-0.1,-0.001`', ), - # hyperopt + # Hyperopt "hyperopt": Arg( '--customhyperopt', help='Specify hyperopt class name (default: `%(default)s`).', - default=constants.DEFAULT_HYPEROPT, metavar='NAME', + default=constants.DEFAULT_HYPEROPT, ), "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', - default=constants.HYPEROPT_EPOCH, type=int, metavar='INT', + default=constants.HYPEROPT_EPOCH, ), "spaces": Arg( '-s', '--spaces', help='Specify which parameters to hyperopt. Space-separated list. ' 'Default: `%(default)s`.', choices=['all', 'buy', 'sell', 'roi', 'stoploss'], - default='all', nargs='+', + default='all', ), "print_all": Arg( '--print-all', help='Print all results, not only the best ones.', action='store_true', - default=False + default=False, ), "hyperopt_jobs": Arg( '-j', '--job-workers', @@ -203,9 +204,9 @@ AVAILABLE_CLI_OPTIONS = { '(hyperopt worker processes). ' 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' 'If 1 is given, no parallel computing code is used at all.', - default=-1, type=int, metavar='JOBS', + default=-1, ), "hyperopt_random_state": Arg( '--random-state', @@ -217,9 +218,9 @@ AVAILABLE_CLI_OPTIONS = { '--min-trades', help="Set minimal desired number of trades for evaluations in the hyperopt " "optimization path (default: 1).", - default=1, type=check_int_positive, metavar='INT', + default=1, ), # List_exchange "print_one_column": Arg( @@ -233,7 +234,6 @@ AVAILABLE_CLI_OPTIONS = { help='Show profits for only these pairs. Pairs are comma-separated.', ), # Download data - "pairs_file": Arg( '--pairs-file', help='File containing a list of pairs to download.', @@ -263,7 +263,7 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), - # Plot_df_options + # Plot dataframe options "indicators1": Arg( '--indicators1', help='Set indicators from your strategy you want in the first row of the graph. ' @@ -280,16 +280,17 @@ AVAILABLE_CLI_OPTIONS = { '--plot-limit', help='Specify tick limit for plotting. Notice: too high values cause huge files. ' 'Default: %(default)s.', - default=750, type=int, + metavar='INT', + default=750, ), "trade_source": Arg( '--trade-source', help='Specify the source for trades (Can be DB or file (backtest file)) ' 'Default: %(default)s', + choices=["DB", "file"], default="file", - choices=["DB", "file"] - ) + ), } ARGS_COMMON = ["loglevel", "logfile", "version", "config", "datadir"] @@ -309,8 +310,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] - -ARGS_LIST_EXCHANGE = ["print_one_column"] +ARGS_LIST_EXCHANGES = ["print_one_column"] ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] @@ -339,7 +339,6 @@ class Arguments(object): """ Arguments Class. Manage the arguments received by the cli """ - def __init__(self, args: Optional[List[str]], description: str) -> None: self.args = args self.parsed_arg: Optional[argparse.Namespace] = None @@ -412,7 +411,7 @@ class Arguments(object): help='Print available exchanges.' ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) - self.build_args(optionlist=ARGS_LIST_EXCHANGE, parser=list_exchanges_cmd) + self.build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) @staticmethod def parse_timerange(text: Optional[str]) -> TimeRange: From d8f133aaf3bcdd902298db8d623cecbcdea3445e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 00:51:01 +0300 Subject: [PATCH 073/191] remove duplicated loglevel option --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index b2d905a5a..f64398443 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -298,7 +298,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["dynamic_whitelist", "db_url", "sd_notify"] -ARGS_COMMON_OPTIMIZE = ["loglevel", "ticker_interval", "timerange", +ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount", "refresh_pairs"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", From a65b5f8e0209cf38b70d558026db443652842cbb Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 01:10:41 +0300 Subject: [PATCH 074/191] make some more arguments positive integers --- freqtrade/arguments.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f64398443..2d3dc0793 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -107,7 +107,8 @@ AVAILABLE_CLI_OPTIONS = { "max_open_trades": Arg( '--max_open_trades', help='Specify max_open_trades to use.', - type=int, + type=check_int_positive, + metavar='INT', ), "stake_amount": Arg( '--stake_amount', @@ -180,7 +181,7 @@ AVAILABLE_CLI_OPTIONS = { "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', - type=int, + type=check_int_positive, metavar='INT', default=constants.HYPEROPT_EPOCH, ), @@ -280,7 +281,7 @@ AVAILABLE_CLI_OPTIONS = { '--plot-limit', help='Specify tick limit for plotting. Notice: too high values cause huge files. ' 'Default: %(default)s.', - type=int, + type=check_int_positive, metavar='INT', default=750, ), From 082065cd5057c233619c7c13bf3a7eeaea4c96f8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 01:20:26 +0300 Subject: [PATCH 075/191] minor cosmetics in arguments.py --- freqtrade/arguments.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 2d3dc0793..8e77cbc0a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -1,7 +1,6 @@ """ This module contains the argument manager class """ - import argparse import os import re @@ -29,9 +28,9 @@ class Arg: self.kwargs = kwargs -# List of available command line arguments +# List of available command line options AVAILABLE_CLI_OPTIONS = { - # Common arguments + # Common options "loglevel": Arg( '-v', '--verbose', help='Verbose mode (-vv for more, -vvv to get all messages).', @@ -122,7 +121,7 @@ AVAILABLE_CLI_OPTIONS = { 'up-to-date data.', action='store_true', ), - # backtesting + # Backtesting "position_stacking": Arg( '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', @@ -223,13 +222,13 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=1, ), - # List_exchange + # List exchanges "print_one_column": Arg( '-1', '--one-column', help='Print exchanges in one column.', action='store_true', ), - # script_options + # Script options "pairs": Arg( '-p', '--pairs', help='Show profits for only these pairs. Pairs are comma-separated.', @@ -264,7 +263,7 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), - # Plot dataframe options + # Plot dataframe "indicators1": Arg( '--indicators1', help='Set indicators from your strategy you want in the first row of the graph. ' @@ -294,7 +293,9 @@ AVAILABLE_CLI_OPTIONS = { ), } + ARGS_COMMON = ["loglevel", "logfile", "version", "config", "datadir"] + ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["dynamic_whitelist", "db_url", "sd_notify"] @@ -326,9 +327,9 @@ ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + class TimeRange(NamedTuple): """ - NamedTuple Defining timerange inputs. + NamedTuple defining timerange inputs. [start/stop]type defines if [start/stop]ts shall be used. - if *type is none, don't use corresponding startvalue. + if *type is None, don't use corresponding startvalue. """ starttype: Optional[str] = None stoptype: Optional[str] = None From 8114d790a5f3b887f85713e9b4201512aed43713 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 01:40:52 +0300 Subject: [PATCH 076/191] commit forgotten loggers.py --- freqtrade/loggers.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 freqtrade/loggers.py diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py new file mode 100644 index 000000000..56dcf2486 --- /dev/null +++ b/freqtrade/loggers.py @@ -0,0 +1,50 @@ +import logging +import sys + +from logging.handlers import RotatingFileHandler +from typing import Any, Dict, List + + +logger = logging.getLogger(__name__) + + +def _set_loggers(verbosity: int = 0) -> None: + """ + Set the logging level for Third party libs + :return: None + """ + + logging.getLogger('requests').setLevel( + logging.INFO if verbosity <= 1 else logging.DEBUG + ) + logging.getLogger("urllib3").setLevel( + logging.INFO if verbosity <= 1 else logging.DEBUG + ) + logging.getLogger('ccxt.base.exchange').setLevel( + logging.INFO if verbosity <= 2 else logging.DEBUG + ) + logging.getLogger('telegram').setLevel(logging.INFO) + + +def setup_logging(config: Dict[str, Any]) -> None: + """ + Process --loglevel, --logfile options + """ + # Log level + verbosity = config['verbosity'] + + # Log to stdout, not stderr + log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] + + if config.get('logfile'): + log_handlers.append(RotatingFileHandler(config['logfile'], + maxBytes=1024 * 1024, # 1Mb + backupCount=10)) + + logging.basicConfig( + level=logging.INFO if verbosity < 1 else logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=log_handlers + ) + _set_loggers(verbosity) + logger.info('Verbosity set to %s', verbosity) From f89b2a18e072859702cb3fdd4cbd1b248189bae4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 02:42:03 +0300 Subject: [PATCH 077/191] fix loglevel in conftest -- it's actually the verbosity level --- freqtrade/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index eb2a8600f..19a7dc423 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -227,7 +227,7 @@ def default_conf(): }, "initial_state": "running", "db_url": "sqlite://", - "loglevel": logging.DEBUG, + "loglevel": 3, } return configuration From 84d3868994d0dd5c7c5144bdd8ffcdd2ca947a34 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 02:53:13 +0300 Subject: [PATCH 078/191] rename loglevel --> verbosity, because it's not logging level --- freqtrade/arguments.py | 4 ++-- freqtrade/configuration.py | 6 +++--- freqtrade/loggers.py | 2 +- freqtrade/tests/conftest.py | 2 +- freqtrade/tests/test_arguments.py | 10 +++++----- freqtrade/tests/test_main.py | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 8e77cbc0a..cb6b30b05 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -31,7 +31,7 @@ class Arg: # List of available command line options AVAILABLE_CLI_OPTIONS = { # Common options - "loglevel": Arg( + "verbosity": Arg( '-v', '--verbose', help='Verbose mode (-vv for more, -vvv to get all messages).', action='count', @@ -294,7 +294,7 @@ AVAILABLE_CLI_OPTIONS = { } -ARGS_COMMON = ["loglevel", "logfile", "version", "config", "datadir"] +ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] ARGS_STRATEGY = ["strategy", "strategy_path"] diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index f8488c438..8ad0fffe9 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -123,11 +123,11 @@ class Configuration(object): def _load_logging_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load logging configuration: - the --loglevel, --logfile options + the -v/--verbose, --logfile options """ # Log level - if 'loglevel' in self.args and self.args.loglevel: - config.update({'verbosity': self.args.loglevel}) + if 'verbosity' in self.args and self.args.verbosity: + config.update({'verbosity': self.args.verbosity}) else: config.update({'verbosity': 0}) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 56dcf2486..b94038a44 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -28,7 +28,7 @@ def _set_loggers(verbosity: int = 0) -> None: def setup_logging(config: Dict[str, Any]) -> None: """ - Process --loglevel, --logfile options + Process -v/--verbose, --logfile options """ # Log level verbosity = config['verbosity'] diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 19a7dc423..888135fa1 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -227,7 +227,7 @@ def default_conf(): }, "initial_state": "running", "db_url": "sqlite://", - "loglevel": 3, + "verbosity": 3, } return configuration diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 8f0dec226..8186892aa 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -19,7 +19,7 @@ def test_parse_args_defaults() -> None: assert args.config == ['config.json'] assert args.strategy_path is None assert args.datadir is None - assert args.loglevel == 0 + assert args.verbosity == 0 def test_parse_args_config() -> None: @@ -42,10 +42,10 @@ def test_parse_args_db_url() -> None: def test_parse_args_verbose() -> None: args = Arguments(['-v'], '').get_parsed_arg() - assert args.loglevel == 1 + assert args.verbosity == 1 args = Arguments(['--verbose'], '').get_parsed_arg() - assert args.loglevel == 1 + assert args.verbosity == 1 def test_common_scripts_options() -> None: @@ -146,7 +146,7 @@ def test_parse_args_backtesting_custom() -> None: call_args = Arguments(args, '').get_parsed_arg() assert call_args.config == ['test_conf.json'] assert call_args.live is True - assert call_args.loglevel == 0 + assert call_args.verbosity == 0 assert call_args.subparser == 'backtesting' assert call_args.func is not None assert call_args.ticker_interval == '1m' @@ -165,7 +165,7 @@ def test_parse_args_hyperopt_custom() -> None: call_args = Arguments(args, '').get_parsed_arg() assert call_args.config == ['test_conf.json'] assert call_args.epochs == 20 - assert call_args.loglevel == 0 + assert call_args.verbosity == 0 assert call_args.subparser == 'hyperopt' assert call_args.spaces == ['buy'] assert call_args.func is not None diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index e6a2006f9..9c578099d 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -27,7 +27,7 @@ def test_parse_args_backtesting(mocker) -> None: call_args = backtesting_mock.call_args[0][0] assert call_args.config == ['config.json'] assert call_args.live is False - assert call_args.loglevel == 0 + assert call_args.verbosity == 0 assert call_args.subparser == 'backtesting' assert call_args.func is not None assert call_args.ticker_interval is None @@ -41,7 +41,7 @@ def test_main_start_hyperopt(mocker) -> 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 == 0 + assert call_args.verbosity == 0 assert call_args.subparser == 'hyperopt' assert call_args.func is not None From 6c2415d32fba4dfb784f3e0c18652b9eb304fa80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2019 06:36:35 +0200 Subject: [PATCH 079/191] Rename parameters from pair to curr --- freqtrade/exchange/exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a41b51e90..8aacc0cee 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -270,14 +270,14 @@ class Exchange(object): f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') - def get_valid_pair_combination(self, paira, pairb) -> str: + def get_valid_pair_combination(self, curr_1, curr_2) -> str: """ - Get valid combination of paira and pairb by trying both combinations. + Get valid pair combination of curr_1 and curr_2 by trying both combinations. """ - for pair in [f"{paira}/{pairb}", f"{pairb}/{paira}"]: + for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: if pair in self.markets and self.markets[pair].get('active'): return pair - raise DependencyException(f"Could not combine {paira} and {pairb} to get a valid pair.") + raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: List[str]) -> None: """ From f7a2428deb15967c9a13bbc0018009ccdc6f906c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 10:13:00 +0300 Subject: [PATCH 080/191] max_open_trades may be -1 --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index cb6b30b05..cb45bcdf5 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -106,7 +106,7 @@ AVAILABLE_CLI_OPTIONS = { "max_open_trades": Arg( '--max_open_trades', help='Specify max_open_trades to use.', - type=check_int_positive, + type=int, metavar='INT', ), "stake_amount": Arg( From 15d2cbd6df240af079c3c667be91219b560596a4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 10:17:01 +0300 Subject: [PATCH 081/191] loggers: wording improved --- freqtrade/loggers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index b94038a44..90b8905e5 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) def _set_loggers(verbosity: int = 0) -> None: """ - Set the logging level for Third party libs + Set the logging level for third party libraries :return: None """ From c4fb0fd6ca532902a09bd7dacf0f6935a462b8f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2019 17:00:34 +0200 Subject: [PATCH 082/191] Don't run the bot with python3 freqtrade * we can either use `python3 -m freqtrade ...` or `freqtrade ...` - and shorter should be better. --- docs/backtesting.md | 18 +++++++++--------- docs/bot-usage.md | 12 ++++++------ docs/deprecated.md | 4 ++-- docs/edge.md | 8 ++++---- docs/faq.md | 4 ++-- docs/hyperopt.md | 4 ++-- docs/installation.md | 2 +- docs/strategy-customization.md | 6 +++--- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2a5163e73..179bcee15 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -24,37 +24,37 @@ The backtesting is very easy with freqtrade. #### With 5 min tickers (Per default) ```bash -python3 freqtrade backtesting +freqtrade backtesting ``` #### With 1 min tickers ```bash -python3 freqtrade backtesting --ticker-interval 1m +freqtrade backtesting --ticker-interval 1m ``` #### Update cached pairs with the latest data ```bash -python3 freqtrade backtesting --refresh-pairs-cached +freqtrade backtesting --refresh-pairs-cached ``` #### With live data (do not alter your testdata files) ```bash -python3 freqtrade backtesting --live +freqtrade backtesting --live ``` #### Using a different on-disk ticker-data source ```bash -python3 freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 +freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 ``` #### With a (custom) strategy file ```bash -python3 freqtrade -s TestStrategy backtesting +freqtrade -s TestStrategy backtesting ``` Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory @@ -62,7 +62,7 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_ #### Exporting trades to file ```bash -python3 freqtrade backtesting --export trades +freqtrade backtesting --export trades ``` The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. @@ -70,7 +70,7 @@ The exported trades can be used for [further analysis](#further-backtest-result- #### Exporting trades to file specifying a custom filename ```bash -python3 freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json +freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json ``` #### Running backtest with smaller testset @@ -81,7 +81,7 @@ you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 freqtrade backtesting --timerange=-200 +freqtrade backtesting --timerange=-200 ``` #### Advanced use of timerange diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 0e01ac0e5..85692ae14 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -49,7 +49,7 @@ The bot allows you to select which configuration file you want to use. Per default, the bot will load the file `./config.json` ```bash -python3 freqtrade -c path/far/far/away/config.json +freqtrade -c path/far/far/away/config.json ``` ### How to use multiple configuration files? @@ -65,13 +65,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua require them): ```bash -python3 freqtrade -c ./config.json +freqtrade -c ./config.json ``` and specify both configuration files when running in the normal Live Trade Mode: ```bash -python3 freqtrade -c ./config.json -c path/to/secrets/keys.config.json +freqtrade -c ./config.json -c path/to/secrets/keys.config.json ``` This could help you hide your private Exchange key and Exchange secrete on you local machine @@ -97,7 +97,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has a strategy class called `AwesomeStrategy` to load it: ```bash -python3 freqtrade --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` If the bot does not find your strategy file, it will display in an error @@ -111,7 +111,7 @@ Learn more about strategy file in This parameter allows you to add an additional strategy lookup path, which gets checked before the default locations (The passed path must be a directory!): ```bash -python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/directory +freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` #### How to install a strategy? @@ -138,7 +138,7 @@ using `--db-url`. This can also be used to specify a custom database in production mode. Example command: ```bash -python3 freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite +freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite ``` ## Backtesting commands diff --git a/docs/deprecated.md b/docs/deprecated.md index c218bd360..b63c8f823 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -13,14 +13,14 @@ on BaseVolume. This value can be changed when you run the script. Get the 20 currencies based on BaseVolume. ```bash -python3 freqtrade --dynamic-whitelist +freqtrade --dynamic-whitelist ``` **Customize the number of currencies to retrieve** Get the 30 currencies based on BaseVolume. ```bash -python3 freqtrade --dynamic-whitelist 30 +freqtrade --dynamic-whitelist 30 ``` **Exception** diff --git a/docs/edge.md b/docs/edge.md index b0e0b2d42..93c15d330 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -209,7 +209,7 @@ Edge will remove sudden pumps in a given market while going through historical d You can run Edge independently in order to see in details the result. Here is an example: ```bash -python3 freqtrade edge +freqtrade edge ``` An example of its output: @@ -235,19 +235,19 @@ An example of its output: ### Update cached pairs with the latest data ```bash -python3 freqtrade edge --refresh-pairs-cached +freqtrade edge --refresh-pairs-cached ``` ### Precising stoploss range ```bash -python3 freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step +freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step ``` ### Advanced use of timerange ```bash -python3 freqtrade edge --timerange=20181110-20181113 +freqtrade edge --timerange=20181110-20181113 ``` Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop. diff --git a/docs/faq.md b/docs/faq.md index c551e3638..22cfcd616 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -47,13 +47,13 @@ compute. We recommend you to run it at least 10.000 epochs: ```bash -python3 freqtrade hyperopt -e 10000 +freqtrade hyperopt -e 10000 ``` or if you want intermediate result to see ```bash -for i in {1..100}; do python3 freqtrade hyperopt -e 100; done +for i in {1..100}; do freqtrade hyperopt -e 100; done ``` #### Why it is so long to run hyperopt? diff --git a/docs/hyperopt.md b/docs/hyperopt.md index a15fd575a..062751908 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -158,7 +158,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -python3 freqtrade -c config.json hyperopt --customhyperopt -e 5000 --spaces all +freqtrade -c config.json hyperopt --customhyperopt -e 5000 --spaces all ``` Use `` as the name of the custom hyperopt used. @@ -184,7 +184,7 @@ you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 freqtrade hyperopt --timerange -200 +freqtrade hyperopt --timerange -200 ``` ### Running Hyperopt with Smaller Search Space diff --git a/docs/installation.md b/docs/installation.md index 544706f87..657273e2f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -184,7 +184,7 @@ python3 -m pip install -e . If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -python3 freqtrade -c config.json +freqtrade -c config.json ``` *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 800012a0f..15f44955b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -13,7 +13,7 @@ Let assume you have a class called `AwesomeStrategy` in the file `awesome-strate 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) ```bash -python3 freqtrade --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` ## Change your strategy @@ -40,7 +40,7 @@ The bot also include a sample strategy called `TestStrategy` you can update: `us You can test it with the parameter: `--strategy TestStrategy` ```bash -python3 freqtrade --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` **For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) @@ -400,7 +400,7 @@ The default buy strategy is located in the file If you want to use a strategy from a different directory you can pass `--strategy-path` ```bash -python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/directory +freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` ### Further strategy ideas From 27cb1a41746d4f6eceefbdea8800014f524b59bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2019 17:08:14 +0200 Subject: [PATCH 083/191] Add FAQ section explaining "module not found" errors --- docs/faq.md | 34 +++++++++++++++++++++++----------- setup.sh | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 22cfcd616..83576af4d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,14 +1,25 @@ # Freqtrade FAQ -### Freqtrade commons +## Freqtrade common issues -#### I have waited 5 minutes, why hasn't the bot made any trades yet?! +### The bot does not start + +Running the bot with `freqtrade --config config.json` does show the output `freqtrade: command not found`. + +This could have the following reasons: + +* The virtual environment is not active + * run `source .env/bin/activate` to activate the virtual environment +* The installation did not work correctly. + * Please check the [Installation documentation](installation.md). + +### I have waited 5 minutes, why hasn't the bot made any trades yet?! Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry position for a trade. Be patient! -#### I have made 12 trades already, why is my total profit negative?! +### I have made 12 trades already, why is my total profit negative?! I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our @@ -19,24 +30,24 @@ of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades. -#### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? +### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? Not quite. Trades are persisted to a database but the configuration is currently only read when the bot is killed and restarted. `/stop` more like pauses. You can stop your bot, adjust settings and start it again. -#### I want to improve the bot with a new strategy +### I want to improve the bot with a new strategy That's great. We have a nice backtesting and hyperoptimizing setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). -#### Is there a setting to only SELL the coins being held and not perform anymore BUYS? +### Is there a setting to only SELL the coins being held and not perform anymore BUYS? You can use the `/forcesell all` command from Telegram. -### Hyperopt module +## Hyperopt module -#### How many epoch do I need to get a good Hyperopt result? +### How many epoch do I need to get a good Hyperopt result? Per default Hyperopts without `-e` or `--epochs` parameter will only run 100 epochs, means 100 evals of your triggers, guards, ... Too few @@ -56,7 +67,7 @@ or if you want intermediate result to see for i in {1..100}; do freqtrade hyperopt -e 100; done ``` -#### Why it is so long to run hyperopt? +### Why it is so long to run hyperopt? Finding a great Hyperopt results takes time. @@ -74,13 +85,14 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th of the search space. -### Edge module +## Edge module -#### Edge implements interesting approach for controlling position size, is there any theory behind it? +### Edge implements interesting approach for controlling position size, is there any theory behind it? The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. You can find further info on expectancy, winrate, risk management and position size in the following sources: + - https://www.tradeciety.com/ultimate-math-guide-for-traders/ - http://www.vantharp.com/tharp-concepts/expectancy.asp - https://samuraitradingacademy.com/trading-expectancy/ diff --git a/setup.sh b/setup.sh index c99a98eb1..fe7110ef6 100755 --- a/setup.sh +++ b/setup.sh @@ -242,7 +242,7 @@ function install() { echo "-------------------------" echo "Run the bot !" echo "-------------------------" - echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade'." + echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade'." } function plot() { From 322227bf67bd8889d334f5d56c398bf85668422a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 9 Jul 2019 00:59:34 +0300 Subject: [PATCH 084/191] fix #2005 --- freqtrade/plot/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ccb932698..7904d42c9 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -42,7 +42,7 @@ def init_plotscript(config): pairs = config["exchange"]["pair_whitelist"] # Set timerange to use - timerange = Arguments.parse_timerange(config["timerange"]) + timerange = Arguments.parse_timerange(config.get("timerange")) tickers = history.load_data( datadir=Path(str(config.get("datadir"))), From c474e2ac86bc6b1a5a448f11a76622e3ed59cd57 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 10 Jul 2019 01:45:02 +0300 Subject: [PATCH 085/191] fix #2008 --- freqtrade/optimize/backtesting.py | 3 +++ freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0fd47ef9a..9abc68fec 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -319,6 +319,9 @@ class Backtesting(object): position_stacking: do we allow position stacking? (default: False) :return: DataFrame """ + # Arguments are long and noisy, so this is commented out. + # Uncomment if you need to debug the backtest() method. +# logger.debug(f"Start backtest, args: {args}") processed = args['processed'] stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7fd9bf5d9..33f28bf6b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -195,7 +195,7 @@ class Hyperopt(Backtesting): { 'stake_amount': self.config['stake_amount'], 'processed': processed, - 'position_stacking': self.config.get('position_stacking', True), + 'position_stacking': self.config.get('position_stacking', False), 'start_date': min_date, 'end_date': max_date, } From 6a431280196685fa10f2033a9021e4aa36409c06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jul 2019 08:49:42 +0200 Subject: [PATCH 086/191] Fix non-rendering docs --- docs/rest-api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 0508f83e4..afecc1d80 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -16,10 +16,10 @@ Sample configuration: }, ``` -!!! Danger: Security warning +!!! Danger Security warning By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. -!!! Danger: Password selection +!!! Danger Password selection Please make sure to select a very strong, unique password to protect your bot from unauthorized access. You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. @@ -62,7 +62,7 @@ docker run -d \ ``` !!! Danger "Security warning" - By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. + By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. ## Consuming the API From e993e010f4bfcf220c23813f287f6f2d19b0ddbe Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Jul 2019 23:02:57 +0300 Subject: [PATCH 087/191] Fix #2013 --- freqtrade/optimize/hyperopt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7fd9bf5d9..0a51a8fb7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -288,9 +288,8 @@ class Hyperopt(Backtesting): (max_date - min_date).days ) - if self.has_space('buy') or self.has_space('sell'): - self.strategy.advise_indicators = \ - self.custom_hyperopt.populate_indicators # type: ignore + self.strategy.advise_indicators = \ + self.custom_hyperopt.populate_indicators # type: ignore preprocessed = self.strategy.tickerdata_to_dataframe(data) From 1bdffcc73bf5cb492766d282edf1fcfeafab6dd1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Jul 2019 21:23:23 +0300 Subject: [PATCH 088/191] make configuration a sep. module, including arguments --- freqtrade/configuration/__init__.py | 8 ++ freqtrade/{ => configuration}/arguments.py | 1 + freqtrade/configuration/check_exchange.py | 48 ++++++++ .../{ => configuration}/configuration.py | 112 +++--------------- freqtrade/configuration/json_schema.py | 53 +++++++++ freqtrade/data/history.py | 2 +- freqtrade/edge/__init__.py | 3 +- freqtrade/main.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/edge_cli.py | 2 +- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/conftest.py | 5 +- freqtrade/tests/data/test_btanalysis.py | 2 +- freqtrade/tests/data/test_history.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 2 +- freqtrade/tests/strategy/test_interface.py | 2 +- freqtrade/tests/test_arguments.py | 5 +- freqtrade/tests/test_configuration.py | 38 +++--- freqtrade/tests/test_main.py | 2 +- freqtrade/tests/test_plotting.py | 2 +- scripts/download_backtest_data.py | 6 +- scripts/plot_dataframe.py | 2 +- scripts/plot_profit.py | 2 +- 24 files changed, 164 insertions(+), 143 deletions(-) create mode 100644 freqtrade/configuration/__init__.py rename freqtrade/{ => configuration}/arguments.py (99%) create mode 100644 freqtrade/configuration/check_exchange.py rename freqtrade/{ => configuration}/configuration.py (78%) create mode 100644 freqtrade/configuration/json_schema.py diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py new file mode 100644 index 000000000..2f74dc597 --- /dev/null +++ b/freqtrade/configuration/__init__.py @@ -0,0 +1,8 @@ +from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401 + +from freqtrade.configuration.arguments import ( # noqa: F401 + ARGS_DOWNLOADER, + ARGS_PLOT_DATAFRAME, + ARGS_PLOT_PROFIT) + +from freqtrade.configuration.configuration import Configuration # noqa: F401 diff --git a/freqtrade/arguments.py b/freqtrade/configuration/arguments.py similarity index 99% rename from freqtrade/arguments.py rename to freqtrade/configuration/arguments.py index cb45bcdf5..3e940ae2a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -5,6 +5,7 @@ import argparse import os import re from typing import List, NamedTuple, Optional + import arrow from freqtrade import __version__, constants diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py new file mode 100644 index 000000000..8dae06f7a --- /dev/null +++ b/freqtrade/configuration/check_exchange.py @@ -0,0 +1,48 @@ +import logging +from typing import Any, Dict + +from freqtrade import OperationalException +from freqtrade.exchange import (is_exchange_bad, is_exchange_available, + is_exchange_officially_supported, available_exchanges) + + +logger = logging.getLogger(__name__) + + +def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: + """ + Check if the exchange name in the config file is supported by Freqtrade + :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. + """ + logger.info("Checking exchange...") + + exchange = config.get('exchange', {}).get('name').lower() + if not is_exchange_available(exchange): + raise OperationalException( + f'Exchange "{exchange}" is not supported by ccxt ' + f'and therefore not available for the bot.\n' + f'The following exchanges are supported by ccxt: ' + f'{", ".join(available_exchanges())}' + ) + + 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 diff --git a/freqtrade/configuration.py b/freqtrade/configuration/configuration.py similarity index 78% rename from freqtrade/configuration.py rename to freqtrade/configuration/configuration.py index 8ad0fffe9..760c470cb 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -8,12 +8,9 @@ import sys from argparse import Namespace from typing import Any, Callable, Dict, Optional -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_available, - is_exchange_officially_supported, available_exchanges) +from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode @@ -22,31 +19,6 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -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']) - - for error in validate_properties( - validator, properties, instance, schema, - ): - yield error - - return validators.extend( - validator_class, {'properties': set_defaults} - ) - - -FreqtradeValidator = _extend_validator(Draft4Validator) - - class Configuration(object): """ Class to read and init the bot configuration @@ -58,6 +30,16 @@ class Configuration(object): self.config: Optional[Dict[str, Any]] = None self.runmode = runmode + def get_config(self) -> Dict[str, Any]: + """ + Return the config. Use this method to get the bot config + :return: Dict: Bot config + """ + if self.config is None: + self.config = self.load_config() + + return self.config + def load_config(self) -> Dict[str, Any]: """ Extract information for sys.argv and load the bot configuration @@ -75,7 +57,7 @@ class Configuration(object): config['internals'] = {} logger.info('Validating configuration ...') - self._validate_config_schema(config) + validate_config_schema(config) self._validate_config_consistency(config) # Set strategy if not specified in config and or if it's non default @@ -185,7 +167,7 @@ class Configuration(object): logger.info(f'Using DB: "{config["db_url"]}"') # Check if the exchange set by the user is supported - self.check_exchange(config) + check_exchange(config) return config @@ -337,24 +319,6 @@ class Configuration(object): logstring='Using trades from: {}') return config - def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: - """ - Validate the configuration follow the Config Schema - :param conf: Config in JSON format - :return: Returns the config if valid, otherwise throw an exception - """ - try: - FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) - return conf - except ValidationError as exception: - logger.critical( - 'Invalid configuration. See config.json.example. Reason: %s', - exception - ) - raise ValidationError( - best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message - ) - def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: """ Validate the configuration consistency @@ -383,51 +347,3 @@ class Configuration(object): raise OperationalException( f'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive_offset in your config.') - - def get_config(self) -> Dict[str, Any]: - """ - Return the config. Use this method to get the bot config - :return: Dict: Bot config - """ - if self.config is None: - self.config = self.load_config() - - return self.config - - 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 - :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. - """ - logger.info("Checking exchange...") - - exchange = config.get('exchange', {}).get('name').lower() - if not is_exchange_available(exchange): - raise OperationalException( - f'Exchange "{exchange}" is not supported by ccxt ' - f'and therefore not available for the bot.\n' - f'The following exchanges are supported by ccxt: ' - f'{", ".join(available_exchanges())}' - ) - - 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 diff --git a/freqtrade/configuration/json_schema.py b/freqtrade/configuration/json_schema.py new file mode 100644 index 000000000..4c6f4a4a0 --- /dev/null +++ b/freqtrade/configuration/json_schema.py @@ -0,0 +1,53 @@ +import logging +from typing import Any, Dict + +from jsonschema import Draft4Validator, validators +from jsonschema.exceptions import ValidationError, best_match + +from freqtrade import constants + + +logger = logging.getLogger(__name__) + + +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']) + + for error in validate_properties( + validator, properties, instance, schema, + ): + yield error + + return validators.extend( + validator_class, {'properties': set_defaults} + ) + + +FreqtradeValidator = _extend_validator(Draft4Validator) + + +def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: + """ + Validate the configuration follow the Config Schema + :param conf: Config in JSON format + :return: Returns the config if valid, otherwise throw an exception + """ + try: + FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) + return conf + except ValidationError as e: + logger.critical( + f"Invalid configuration. See config.json.example. Reason: {e}" + ) + raise ValidationError( + best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message + ) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 2a0d9b15e..f600615df 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -16,7 +16,7 @@ import arrow from pandas import DataFrame from freqtrade import OperationalException, misc -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange import Exchange, timeframe_to_minutes diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4bc8bb493..7085663d6 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -10,8 +10,7 @@ import utils_find_1st as utf1st from pandas import DataFrame from freqtrade import constants, OperationalException -from freqtrade.arguments import Arguments -from freqtrade.arguments import TimeRange +from freqtrade.configuration import Arguments, TimeRange from freqtrade.data import history from freqtrade.strategy.interface import SellType diff --git a/freqtrade/main.py b/freqtrade/main.py index f02159a0e..a96fd43c5 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -15,7 +15,7 @@ from argparse import Namespace from typing import Any, List from freqtrade import OperationalException -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.worker import Worker diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0fd47ef9a..0592790ba 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -12,7 +12,7 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 231493e4d..8d1fa381b 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -9,7 +9,7 @@ from tabulate import tabulate from freqtrade import constants from freqtrade.edge import Edge -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7fd9bf5d9..09f99ab7b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -18,7 +18,7 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 7904d42c9..dde6f78f0 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional import pandas as pd -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.data import history from freqtrade.data.btanalysis import (combine_tickers_with_mean, create_cum_profit, load_trades) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 888135fa1..be1139863 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -14,7 +14,7 @@ import pytest from telegram import Chat, Message, Update from freqtrade import constants, persistence -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange @@ -22,6 +22,7 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker + logging.getLogger('').setLevel(logging.INFO) @@ -39,7 +40,7 @@ def log_has_re(line, logs): False) -def get_args(args) -> List[str]: +def get_args(args): return Arguments(args, '').get_parsed_arg() diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index e8872f9a4..e80840009 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -4,7 +4,7 @@ import pytest from arrow import Arrow from pandas import DataFrame, to_datetime -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.configuration import Arguments, TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, combine_tickers_with_mean, create_cum_profit, diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 46bcf06c4..424333e99 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -12,7 +12,7 @@ import pytest from pandas import DataFrame from freqtrade import OperationalException -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 498a04db5..bea36c30c 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -11,7 +11,7 @@ import pytest from arrow import Arrow from freqtrade import DependencyException, constants -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index e92cb7b1c..ee8c8ddd4 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import arrow from pandas import DataFrame -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.persistence import Trade diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 8186892aa..ff1b0e6b0 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,8 +3,9 @@ import argparse import pytest -from freqtrade.arguments import (ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME, - Arguments, TimeRange, check_int_positive) +from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME +from freqtrade.configuration.arguments import check_int_positive # Parse common command-line-arguments. Used for all tools diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index c5e60be7f..156635128 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -11,8 +11,9 @@ import pytest from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants -from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration +from freqtrade.configuration import Arguments, Configuration +from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode @@ -32,24 +33,21 @@ def test_load_config_invalid_pair(default_conf) -> None: default_conf['exchange']['pair_whitelist'].append('ETH-BTC') with pytest.raises(ValidationError, match=r'.*does not match.*'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(default_conf) + validate_config_schema(default_conf) def test_load_config_missing_attributes(default_conf) -> None: default_conf.pop('exchange') with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(default_conf) + validate_config_schema(default_conf) def test_load_config_incorrect_stake_amount(default_conf) -> None: default_conf['stake_amount'] = 'fake' with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(default_conf) + validate_config_schema(default_conf) def test_load_config_file(default_conf, mocker, caplog) -> None: @@ -469,25 +467,23 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_check_exchange(default_conf, caplog) -> None: - configuration = Configuration(Namespace()) - # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'BITTREX'}) - assert configuration.check_exchange(default_conf) + assert 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 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 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) @@ -495,7 +491,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) - assert not configuration.check_exchange(default_conf) + assert not check_exchange(default_conf) assert log_has_re(r"Exchange .* is known to not work with the bot yet\. " r"Use it only for development and testing purposes\.", caplog.record_tuples) @@ -503,7 +499,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange with check_for_bad=False default_conf.get('exchange').update({'name': 'bitmex'}) - assert configuration.check_exchange(default_conf, False) + assert check_exchange(default_conf, False) assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " r"by the Freqtrade development team\. .*", caplog.record_tuples) @@ -511,14 +507,13 @@ def test_check_exchange(default_conf, caplog) -> None: # Test an invalid exchange default_conf.get('exchange').update({'name': 'unknown_exchange'}) - configuration.config = default_conf with pytest.raises( OperationalException, match=r'.*Exchange "unknown_exchange" is not supported by ccxt ' r'and therefore not available for the bot.*' ): - configuration.check_exchange(default_conf) + check_exchange(default_conf) def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: @@ -656,8 +651,7 @@ def test_load_config_default_exchange(all_conf) -> None: with pytest.raises(ValidationError, match=r'\'exchange\' is a required property'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) + validate_config_schema(all_conf) def test_load_config_default_exchange_name(all_conf) -> None: @@ -671,8 +665,7 @@ def test_load_config_default_exchange_name(all_conf) -> None: with pytest.raises(ValidationError, match=r'\'name\' is a required property'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) + validate_config_schema(all_conf) @pytest.mark.parametrize("keys", [("exchange", "sandbox", False), @@ -695,7 +688,6 @@ def test_load_config_default_subkeys(all_conf, keys) -> None: assert subkey not in all_conf[key] - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) + validate_config_schema(all_conf) assert subkey in all_conf[key] assert all_conf[key][subkey] == keys[2] diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 9c578099d..2f778e5fa 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import pytest from freqtrade import OperationalException -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index cef229d19..cb1852223 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import plotly.graph_objs as go from plotly import tools -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.configuration import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.plot.plotting import (add_indicators, add_profit, diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 472b3ef4e..60a9e2cbc 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -8,8 +8,10 @@ import sys from pathlib import Path from typing import Any, Dict, List -from freqtrade.arguments import Arguments, TimeRange, ARGS_DOWNLOADER +from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import ARGS_DOWNLOADER from freqtrade.configuration import Configuration +from freqtrade.configuration.check_exchange import check_exchange from freqtrade.data.history import download_pair_history from freqtrade.exchange import Exchange from freqtrade.misc import deep_merge_dicts @@ -79,7 +81,7 @@ if args.config and args.exchange: "using exchange settings from the configuration file.") # Check if the exchange set by the user is supported -configuration.check_exchange(config) +check_exchange(config) configuration._load_datadir_config(config) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 1e2d9f248..f0bc30366 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -18,7 +18,7 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments +from freqtrade.configuration import Arguments, ARGS_PLOT_DATAFRAME from freqtrade.data.btanalysis import extract_trades_of_period from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (init_plotscript, generate_candlestick_graph, diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 7442ef155..632b7879a 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -8,7 +8,7 @@ import logging import sys from typing import Any, Dict, List -from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments +from freqtrade.configuration import Arguments, ARGS_PLOT_PROFIT from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import init_plotscript, generate_profit_graph, store_plot_file from freqtrade.state import RunMode From 94e6fb89b327dbc7c114cdb6fef93a94150b76a2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 12 Jul 2019 00:39:42 +0300 Subject: [PATCH 089/191] tests happy --- freqtrade/tests/conftest.py | 14 ++++ freqtrade/tests/optimize/test_backtesting.py | 30 +++---- freqtrade/tests/optimize/test_edge_cli.py | 18 ++--- freqtrade/tests/optimize/test_hyperopt.py | 38 +++------ freqtrade/tests/test_configuration.py | 83 +++++++------------- freqtrade/tests/test_main.py | 33 ++------ 6 files changed, 76 insertions(+), 140 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index be1139863..9be610090 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -44,6 +44,20 @@ def get_args(args): return Arguments(args, '').get_parsed_arg() +def patched_configuration_open(mocker, config): + file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( + read_data=json.dumps(config) + )) + return file_mock + + +def patched_configuration_load_config_file(mocker, config) -> None: + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._load_config_file', + lambda *args, **kwargs: config + ) + + 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 bea36c30c..d05d07a1b 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -22,7 +22,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, + patched_configuration_open) def trim_dictlist(dict_list, num): @@ -165,9 +166,7 @@ def _trend_alternate(dataframe=None, metadata=None): # Unit tests def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -205,10 +204,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_open(mocker, default_conf) + mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) args = [ '--config', 'config.json', @@ -276,9 +273,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -295,9 +290,8 @@ def test_start(mocker, fee, default_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + args = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', @@ -828,9 +822,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): patch_exchange(mocker, api_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -880,9 +872,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): gen_strattable_mock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', gen_strattable_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 7cc41b095..c81a8dce8 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -8,13 +8,12 @@ from freqtrade.edge import PairInfo from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode -from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, + patched_configuration_open) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -46,10 +45,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(edge_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_open(mocker, edge_conf) + mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) args = [ '--config', 'config.json', @@ -92,9 +89,8 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(edge_conf) - )) + patched_configuration_open(mocker, edge_conf) + args = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index e5f87b022..a83b4c0d0 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -16,7 +16,8 @@ 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 get_args, log_has, log_has_re, patch_exchange +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, + patched_configuration_load_config_file, patched_configuration_open) @pytest.fixture(scope='function') @@ -44,9 +45,7 @@ def create_trials(mocker, hyperopt) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -82,10 +81,8 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_open(mocker, default_conf) + mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) args = [ '--config', 'config.json', @@ -148,11 +145,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo def test_hyperoptresolver(mocker, default_conf, caplog) -> None: + patched_configuration_load_config_file(mocker, default_conf) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) hyperopts = DefaultHyperOpts delattr(hyperopts, 'populate_buy_trend') delattr(hyperopts, 'populate_sell_trend') @@ -172,10 +166,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: def test_start(mocker, default_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) @@ -198,10 +189,7 @@ def test_start(mocker, default_conf, caplog) -> None: def test_start_no_data(mocker, default_conf, caplog) -> None: - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={})) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', @@ -226,10 +214,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: def test_start_failure(mocker, default_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) @@ -250,10 +235,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None: def test_start_filelock(mocker, default_conf, caplog) -> None: start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 156635128..09ba6cd54 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -17,7 +17,7 @@ from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, log_has_re +from freqtrade.tests.conftest import log_has, log_has_re, patched_configuration_open @pytest.fixture(scope="function") @@ -51,9 +51,7 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None: - file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + file_mock = patched_configuration_open(mocker, default_conf) configuration = Configuration(Namespace()) validated_conf = configuration._load_config_file('somefile') @@ -63,9 +61,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = 0 - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -87,7 +83,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: config_files = [conf1, conf2] configsmock = MagicMock(side_effect=config_files) - mocker.patch('freqtrade.configuration.Configuration._load_config_file', configsmock) + mocker.patch('freqtrade.configuration.configuration.Configuration._load_config_file', configsmock) arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] args = Arguments(arg_list, '').get_parsed_arg() @@ -107,9 +103,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = -1 - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -124,7 +118,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> def test_load_config_file_exception(mocker) -> None: mocker.patch( - 'freqtrade.configuration.open', + 'freqtrade.configuration.configuration.open', MagicMock(side_effect=FileNotFoundError('File not found')) ) configuration = Configuration(Namespace()) @@ -134,9 +128,7 @@ def test_load_config_file_exception(mocker) -> None: def test_load_config(default_conf, mocker) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -148,9 +140,8 @@ def test_load_config(default_conf, mocker) -> None: def test_load_config_with_params(default_conf, mocker) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + arglist = [ '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', @@ -171,9 +162,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = False conf["db_url"] = "sqlite:///path/to/db.sqlite" - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) - )) + patched_configuration_open(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -189,9 +178,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = "sqlite:///path/to/db.sqlite" - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) - )) + patched_configuration_open(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -207,9 +194,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = False del conf["db_url"] - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) - )) + patched_configuration_open(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -227,9 +212,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = DEFAULT_DB_PROD_URL - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) - )) + patched_configuration_open(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -247,9 +230,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None: 'strategy': 'CustomStrategy', 'strategy_path': '/tmp/strategies', }) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -260,9 +241,8 @@ def test_load_custom_strategy(default_conf, mocker) -> None: def test_show_info(default_conf, mocker, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + arglist = [ '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', @@ -285,9 +265,8 @@ def test_show_info(default_conf, mocker, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + arglist = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', @@ -325,10 +304,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_open(mocker, default_conf) + mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) arglist = [ '--config', 'config.json', @@ -391,9 +368,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non """ Test setup_configuration() function """ - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) arglist = [ '--config', 'config.json', @@ -441,9 +416,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + arglist = [ 'hyperopt', '--epochs', '10', @@ -517,8 +491,8 @@ def test_check_exchange(default_conf, caplog) -> None: def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf))) + patched_configuration_open(mocker, default_conf) + # Prevent setting loggers mocker.patch('freqtrade.loggers._set_loggers', MagicMock) arglist = ['-vvv'] @@ -570,8 +544,7 @@ def test_set_loggers() -> None: def test_set_logfile(default_conf, mocker): - mocker.patch('freqtrade.configuration.open', - mocker.mock_open(read_data=json.dumps(default_conf))) + patched_configuration_open(mocker, default_conf) arglist = [ '--logfile', 'test_file.log', @@ -588,9 +561,7 @@ def test_set_logfile(default_conf, mocker): def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: default_conf['forcebuy_enable'] = True - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 2f778e5fa..8e5a24f8e 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -10,7 +10,8 @@ from freqtrade.configuration import Arguments from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State -from freqtrade.tests.conftest import log_has, patch_exchange +from freqtrade.tests.conftest import (log_has, patch_exchange, + patched_configuration_load_config_file) from freqtrade.worker import Worker @@ -50,10 +51,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -70,10 +68,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt)) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -93,10 +88,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: 'freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')) ) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -118,10 +110,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: State.RUNNING, OperationalException("Oh snap!")]) mocker.patch('freqtrade.worker.Worker._worker', worker_mock) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) @@ -145,10 +134,7 @@ def test_reconfigure(mocker, default_conf) -> None: 'freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')) ) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -159,10 +145,7 @@ def test_reconfigure(mocker, default_conf) -> None: # Renew mock to return modified data conf = deepcopy(default_conf) conf['stake_amount'] += 1 - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: conf - ) + patched_configuration_load_config_file(mocker, conf) worker._config = conf # reconfigure should return a new instance From 7e103e34f81c06e5702b4202e79cc5966d0dea4c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 12 Jul 2019 01:41:09 +0300 Subject: [PATCH 090/191] flake happy --- freqtrade/tests/conftest.py | 1 - freqtrade/tests/optimize/test_backtesting.py | 8 +++++--- freqtrade/tests/optimize/test_edge_cli.py | 8 +++++--- freqtrade/tests/optimize/test_hyperopt.py | 9 ++++++--- freqtrade/tests/test_configuration.py | 11 ++++++++--- freqtrade/tests/test_main.py | 2 +- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 9be610090..f57e58774 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -6,7 +6,6 @@ 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 import arrow diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index d05d07a1b..55405ea3d 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument -import json import math import random from unittest.mock import MagicMock @@ -23,7 +22,7 @@ from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_open) + patched_configuration_open) def trim_dictlist(dict_list, num): @@ -205,7 +204,10 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) - mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._create_datadir', + lambda s, c, x: x + ) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index c81a8dce8..470a831fb 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -1,7 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103, C0330 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments -import json from unittest.mock import MagicMock from freqtrade.edge import PairInfo @@ -9,7 +8,7 @@ from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_open) + patched_configuration_open) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: @@ -46,7 +45,10 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: patched_configuration_open(mocker, edge_conf) - mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._create_datadir', + lambda s, c, x: x + ) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a83b4c0d0..386893572 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 -import json import os from datetime import datetime from unittest.mock import MagicMock @@ -17,7 +16,8 @@ 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 (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file, patched_configuration_open) + patched_configuration_load_config_file, + patched_configuration_open) @pytest.fixture(scope='function') @@ -82,7 +82,10 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) - mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._create_datadir', + lambda s, c, x: x + ) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 09ba6cd54..d25d36526 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name -import json import logging from argparse import Namespace from copy import deepcopy @@ -83,7 +82,10 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: config_files = [conf1, conf2] configsmock = MagicMock(side_effect=config_files) - mocker.patch('freqtrade.configuration.configuration.Configuration._load_config_file', configsmock) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._load_config_file', + configsmock + ) arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] args = Arguments(arg_list, '').get_parsed_arg() @@ -305,7 +307,10 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) - mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._create_datadir', + lambda s, c, x: x + ) arglist = [ '--config', 'config.json', diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 8e5a24f8e..bcaad4d7d 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -11,7 +11,7 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State from freqtrade.tests.conftest import (log_has, patch_exchange, - patched_configuration_load_config_file) + patched_configuration_load_config_file) from freqtrade.worker import Worker From bbfbd87a9fe86b371331cb496848beddec5da199 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 12 Jul 2019 03:26:27 +0300 Subject: [PATCH 091/191] move create_datadir() to separate file --- freqtrade/configuration/configuration.py | 17 +++-------------- freqtrade/configuration/create_datadir.py | 18 ++++++++++++++++++ freqtrade/tests/optimize/test_backtesting.py | 4 ++-- freqtrade/tests/optimize/test_edge_cli.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 4 ++-- freqtrade/tests/test_configuration.py | 10 +++++----- 6 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 freqtrade/configuration/create_datadir.py diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 760c470cb..4959c2adc 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -3,13 +3,13 @@ This module contains the configuration class """ import json import logging -import os import sys from argparse import Namespace from typing import Any, Callable, Dict, Optional from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.create_datadir import create_datadir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts @@ -171,17 +171,6 @@ class Configuration(object): return config - def _create_datadir(self, config: Dict[str, Any], datadir: Optional[str] = None) -> str: - if not datadir: - # set datadir - exchange_name = config.get('exchange', {}).get('name').lower() - datadir = os.path.join('user_data', 'data', exchange_name) - - if not os.path.isdir(datadir): - os.makedirs(datadir) - logger.info(f'Created data directory: {datadir}') - return datadir - def _args_to_config(self, config: Dict[str, Any], argname: str, logstring: str, logfun: Optional[Callable] = None) -> None: """ @@ -207,9 +196,9 @@ class Configuration(object): the --datadir option """ if 'datadir' in self.args and self.args.datadir: - config.update({'datadir': self._create_datadir(config, self.args.datadir)}) + config.update({'datadir': create_datadir(config, self.args.datadir)}) else: - config.update({'datadir': self._create_datadir(config, None)}) + config.update({'datadir': create_datadir(config, None)}) logger.info('Using data directory: %s ...', config.get('datadir')) def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/configuration/create_datadir.py b/freqtrade/configuration/create_datadir.py new file mode 100644 index 000000000..ecb59bc84 --- /dev/null +++ b/freqtrade/configuration/create_datadir.py @@ -0,0 +1,18 @@ +import logging +import os +from typing import Any, Dict, Optional + + +logger = logging.getLogger(__name__) + + +def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str: + if not datadir: + # set datadir + exchange_name = config.get('exchange', {}).get('name').lower() + datadir = os.path.join('user_data', 'data', exchange_name) + + if not os.path.isdir(datadir): + os.makedirs(datadir) + logger.info(f'Created data directory: {datadir}') + return datadir diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 55405ea3d..ee4d23d35 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -205,8 +205,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._create_datadir', - lambda s, c, x: x + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x ) args = [ diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 470a831fb..582271af2 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -46,8 +46,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: patched_configuration_open(mocker, edge_conf) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._create_datadir', - lambda s, c, x: x + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x ) args = [ diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 386893572..7c00c3023 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -83,8 +83,8 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._create_datadir', - lambda s, c, x: x + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x ) args = [ diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index d25d36526..f6d2deeaa 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -12,6 +12,7 @@ from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants from freqtrade.configuration import Arguments, Configuration from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.create_datadir import create_datadir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers @@ -308,8 +309,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._create_datadir', - lambda s, c, x: x + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x ) arglist = [ @@ -580,12 +581,11 @@ def test_validate_default_conf(default_conf) -> None: validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) -def test__create_datadir(mocker, default_conf, caplog) -> None: +def test_create_datadir(mocker, default_conf, caplog) -> None: mocker.patch('os.path.isdir', MagicMock(return_value=False)) md = MagicMock() mocker.patch('os.makedirs', md) - cfg = Configuration(Namespace()) - cfg._create_datadir(default_conf, '/foo/bar') + create_datadir(default_conf, '/foo/bar') assert md.call_args[0][0] == "/foo/bar" assert log_has('Created data directory: /foo/bar', caplog.record_tuples) From b499e74502d9b107b1fafb73a57f29fbabd7d105 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 12 Jul 2019 23:45:49 +0300 Subject: [PATCH 092/191] minor improvements to resolvers --- freqtrade/resolvers/exchange_resolver.py | 8 +++--- freqtrade/resolvers/hyperopt_resolver.py | 16 ++++++----- freqtrade/resolvers/iresolver.py | 11 ++++---- freqtrade/resolvers/pairlist_resolver.py | 18 ++++++------ freqtrade/resolvers/strategy_resolver.py | 21 ++++++++------ freqtrade/tests/pairlist/test_pairlist.py | 6 ++-- freqtrade/tests/strategy/test_strategy.py | 34 ++++++++++++----------- 7 files changed, 62 insertions(+), 52 deletions(-) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 089f6306f..5f1708ee6 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -45,13 +45,13 @@ class ExchangeResolver(IResolver): exchange = ex_class(kwargs['config']) if exchange: - logger.info("Using resolved exchange %s", exchange_name) + logger.info(f"Using resolved exchange '{exchange_name}'...") return exchange except AttributeError: - # Pass and raise ImportError instead + # Pass and raise OperationalException instead pass raise ImportError( - "Impossible to load Exchange '{}'. This class does not exist" - " or contains Python code errors".format(exchange_name) + f"Impossible to load Exchange '{exchange_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 9333bb09a..30e097f3f 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -7,6 +7,7 @@ import logging from pathlib import Path from typing import Optional, Dict +from freqtrade import OperationalException from freqtrade.constants import DEFAULT_HYPEROPT from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.resolvers import IResolver @@ -63,15 +64,16 @@ class HyperOptResolver(IResolver): for _path in abs_paths: try: - hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, - object_name=hyperopt_name) + (hyperopt, module_path) = self._search_object(directory=_path, + object_type=IHyperOpt, + object_name=hyperopt_name) if hyperopt: - logger.info("Using resolved hyperopt %s from '%s'", hyperopt_name, _path) + logger.info(f"Using resolved hyperopt {hyperopt_name} from '{module_path}'...") return hyperopt except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - raise ImportError( - "Impossible to load Hyperopt '{}'. This class does not exist" - " or contains Python code errors".format(hyperopt_name) + raise OperationalException( + f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 86b9a799b..1065abba7 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Optional, Type, Any +from typing import Any, Optional, Tuple, Type, Union logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ class IResolver(object): @staticmethod def _search_object(directory: Path, object_type, object_name: str, - kwargs: dict = {}) -> Optional[Any]: + kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: """ Search for the objectname in the given directory :param directory: relative or absolute directory path @@ -57,9 +57,10 @@ class IResolver(object): if not str(entry).endswith('.py'): logger.debug('Ignoring %s', entry) continue + module_path = Path.resolve(directory.joinpath(entry)) obj = IResolver._get_valid_object( - object_type, Path.resolve(directory.joinpath(entry)), object_name + object_type, module_path, object_name ) if obj: - return obj(**kwargs) - return None + return (obj(**kwargs), module_path) + return (None, None) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 4306a9669..ac67ca496 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -6,6 +6,7 @@ This module load custom hyperopts import logging from pathlib import Path +from freqtrade import OperationalException from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import IResolver @@ -44,16 +45,17 @@ class PairListResolver(IResolver): for _path in abs_paths: try: - pairlist = self._search_object(directory=_path, object_type=IPairList, - object_name=pairlist_name, - kwargs=kwargs) + (pairlist, module_path) = self._search_object(directory=_path, + object_type=IPairList, + object_name=pairlist_name, + kwargs=kwargs) if pairlist: - logger.info("Using resolved pairlist %s from '%s'", pairlist_name, _path) + logger.info(f"Using resolved pairlist {pairlist_name} from '{module_path}'...") return pairlist except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - raise ImportError( - "Impossible to load Pairlist '{}'. This class does not exist" - " or contains Python code errors".format(pairlist_name) + raise OperationalException( + f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 47218bef4..4a5604db8 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -11,7 +11,7 @@ from inspect import getfullargspec from pathlib import Path from typing import Dict, Optional -from freqtrade import constants +from freqtrade import constants, OperationalException from freqtrade.resolvers import IResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.interface import IStrategy @@ -149,10 +149,12 @@ class StrategyResolver(IResolver): for _path in abs_paths: try: - strategy = self._search_object(directory=_path, object_type=IStrategy, - object_name=strategy_name, kwargs={'config': config}) + (strategy, module_path) = self._search_object(directory=_path, + object_type=IStrategy, + object_name=strategy_name, + kwargs={'config': config}) if strategy: - logger.info("Using resolved strategy %s from '%s'", strategy_name, _path) + logger.info(f"Using resolved strategy {strategy_name} from '{module_path}'...") strategy._populate_fun_len = len( getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) @@ -161,11 +163,12 @@ class StrategyResolver(IResolver): return import_strategy(strategy, config=config) except TypeError as e: logger.warning( - f"Impossible to load strategy '{strategy}' from {_path}. Error: {e}") + f"Impossible to load strategy '{strategy_name}' from {module_path}. " + f"Error: {e}") except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - raise ImportError( - f"Impossible to load Strategy '{strategy_name}'. This class does not exist" - " or contains Python code errors" + raise OperationalException( + f"Impossible to load Strategy '{strategy_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/tests/pairlist/test_pairlist.py b/freqtrade/tests/pairlist/test_pairlist.py index 38a8d78c7..e7439bb51 100644 --- a/freqtrade/tests/pairlist/test_pairlist.py +++ b/freqtrade/tests/pairlist/test_pairlist.py @@ -34,9 +34,9 @@ def whitelist_conf(default_conf): def test_load_pairlist_noexist(mocker, markets, default_conf): freqtradebot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - with pytest.raises(ImportError, - match=r"Impossible to load Pairlist 'NonexistingPairList'." - r" This class does not exist or contains Python code errors"): + with pytest.raises(OperationalException, + match=r"Impossible to load Pairlist 'NonexistingPairList'. " + r"This class does not exist or contains Python code errors."): PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 8971b041c..904f84946 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -9,6 +9,7 @@ from unittest.mock import Mock import pytest from pandas import DataFrame +from freqtrade import OperationalException from freqtrade.resolvers import StrategyResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.default_strategy import DefaultStrategy @@ -44,21 +45,22 @@ def test_import_strategy(caplog): def test_search_strategy(): default_config = {} default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() - assert isinstance( - StrategyResolver._search_object( - directory=default_location, - object_type=IStrategy, - kwargs={'config': default_config}, - object_name='DefaultStrategy' - ), - IStrategy + + s, _ = StrategyResolver._search_object( + directory=default_location, + object_type=IStrategy, + kwargs={'config': default_config}, + object_name='DefaultStrategy' ) - assert StrategyResolver._search_object( + assert isinstance(s, IStrategy) + + s, _ = StrategyResolver._search_object( directory=default_location, object_type=IStrategy, kwargs={'config': default_config}, object_name='NotFoundStrategy' - ) is None + ) + assert s is None def test_load_strategy(result): @@ -85,18 +87,18 @@ def test_load_strategy_invalid_directory(result, caplog): def test_load_not_found_strategy(): strategy = StrategyResolver() - with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'NotFoundStrategy'." - r" This class does not exist or contains Python code errors"): + with pytest.raises(OperationalException, + match=r"Impossible to load Strategy 'NotFoundStrategy'. " + r"This class does not exist or contains Python code errors."): strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) def test_load_staticmethod_importerror(mocker, caplog): mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock( side_effect=TypeError("can't pickle staticmethod objects"))) - with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'DefaultStrategy'." - r" This class does not exist or contains Python code errors"): + with pytest.raises(OperationalException, + match=r"Impossible to load Strategy 'DefaultStrategy'. " + r"This class does not exist or contains Python code errors."): StrategyResolver() assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples) From 2e1269c4740d5d287866d382f70a30a91d6b5ec4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 13:30:57 +0200 Subject: [PATCH 093/191] Revert comment for Exception that's not changed --- freqtrade/resolvers/exchange_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 5f1708ee6..6fb12a65f 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -48,7 +48,7 @@ class ExchangeResolver(IResolver): logger.info(f"Using resolved exchange '{exchange_name}'...") return exchange except AttributeError: - # Pass and raise OperationalException instead + # Pass and raise ImportError instead pass raise ImportError( From 9887cb997ed18907995f60db8ee795d407a86ded Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 14:17:09 +0200 Subject: [PATCH 094/191] Check if Price is needed for market orders This is currently the case for: cex, coinex, cointiger, fcoin, fcoinjp, hadax, huobipro, huobiru, uex, --- freqtrade/exchange/exchange.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ec2832cbf..65f013a03 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -387,7 +387,9 @@ class Exchange(object): try: # Set the precision for amount and price(rate) as accepted by the exchange amount = self.symbol_amount_prec(pair, amount) - rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None + needs_price = (ordertype != 'market' + or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) + rate = self.symbol_price_prec(pair, rate) if needs_price else None return self._api.create_order(pair, ordertype, side, amount, rate, params) @@ -395,12 +397,12 @@ class Exchange(object): except ccxt.InsufficientFunds as e: raise DependencyException( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' + f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' f'Message: {e}') except ccxt.InvalidOrder as e: raise DependencyException( f'Could not create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' + f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' f'Message: {e}') except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( From 25822d17177e73f8b3346e14d9d21c038e93b0ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 14:18:30 +0200 Subject: [PATCH 095/191] Add empty options dict to all tests using create_order --- freqtrade/tests/exchange/test_exchange.py | 5 +++++ freqtrade/tests/exchange/test_kraken.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index bc1e7912c..9de605ffd 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -552,6 +552,7 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name): def test_create_order(default_conf, mocker, side, ordertype, rate, exchange_name): api_mock = MagicMock() order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -592,6 +593,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'market' time_in_force = 'gtc' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -659,6 +661,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -719,6 +722,7 @@ def test_sell_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_type = 'market' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -783,6 +787,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): 'foo': 'bar' } }) + api_mock.options = {} default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) diff --git a/freqtrade/tests/exchange/test_kraken.py b/freqtrade/tests/exchange/test_kraken.py index 8b81a08a9..8f476affb 100644 --- a/freqtrade/tests/exchange/test_kraken.py +++ b/freqtrade/tests/exchange/test_kraken.py @@ -11,6 +11,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'limit' time_in_force = 'ioc' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -42,6 +43,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_type = 'market' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { From a8f3f2bc1a85d56b360dead4dfba36082afe62dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 14:23:23 +0200 Subject: [PATCH 096/191] Extend test to cover market orders with price too --- freqtrade/tests/exchange/test_exchange.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 9de605ffd..a4f1bca18 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -543,16 +543,17 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name): ("buy"), ("sell") ]) -@pytest.mark.parametrize("ordertype,rate", [ - ("market", None), - ("limit", 200), - ("stop_loss_limit", 200) +@pytest.mark.parametrize("ordertype,rate,marketprice", [ + ("market", None, None), + ("market", 200, True), + ("limit", 200, None), + ("stop_loss_limit", 200, None) ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_create_order(default_conf, mocker, side, ordertype, rate, exchange_name): +def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name): api_mock = MagicMock() order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) - api_mock.options = {} + api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { From efbc7cccb18f8124c8ab9e4900c4fb6f80907ed8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jul 2019 20:56:17 +0300 Subject: [PATCH 097/191] enable --dmmp for hyperopt --- freqtrade/optimize/hyperopt.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 33f28bf6b..03bafd1ad 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -190,11 +190,21 @@ class Hyperopt(Backtesting): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) + + # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): + max_open_trades = self.config['max_open_trades'] + else: + logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') + max_open_trades = 0 + min_date, max_date = get_timeframe(processed) + results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, + 'max_open_trades': max_open_trades, 'position_stacking': self.config.get('position_stacking', False), 'start_date': min_date, 'end_date': max_date, From 65f77306d3cb55d5e9365c606bfd88cf01318260 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jul 2019 21:00:48 +0300 Subject: [PATCH 098/191] using logger.debug, info was too noisy --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03bafd1ad..6f113d0e0 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -195,7 +195,7 @@ class Hyperopt(Backtesting): if self.config.get('use_max_market_positions', True): max_open_trades = self.config['max_open_trades'] else: - logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') + logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') max_open_trades = 0 min_date, max_date = get_timeframe(processed) From 4238ee090d4effc75ad14427f335cc254b5c5861 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 20:05:28 +0200 Subject: [PATCH 099/191] Cleanup some code after deepcode.ai suggestions --- freqtrade/data/dataprovider.py | 9 ++++++--- freqtrade/tests/strategy/test_strategy.py | 4 ++-- freqtrade/tests/test_talib.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 2852cbcb0..b87589df7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -17,7 +17,7 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -class DataProvider(object): +class DataProvider(): def __init__(self, config: dict, exchange: Exchange) -> None: self._config = config @@ -81,11 +81,14 @@ class DataProvider(object): # TODO: Implement me pass - def orderbook(self, pair: str, max: int): + def orderbook(self, pair: str, maximum: int): """ return latest orderbook data + :param pair: pair to get the data for + :param maximum: Maximum number of orderbook entries to query + :return: dict including bids/asks with a total of `maximum` entries. """ - return self._exchange.get_order_book(pair, max) + return self._exchange.get_order_book(pair, maximum) @property def runmode(self) -> RunMode: diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 904f84946..9a2c950e5 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -69,8 +69,8 @@ def test_load_strategy(result): def test_load_strategy_base64(result): - with open("freqtrade/tests/strategy/test_strategy.py", "r") as file: - encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8") + with open("freqtrade/tests/strategy/test_strategy.py", "rb") as file: + encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) diff --git a/freqtrade/tests/test_talib.py b/freqtrade/tests/test_talib.py index 093c3023c..2c7f73eb1 100644 --- a/freqtrade/tests/test_talib.py +++ b/freqtrade/tests/test_talib.py @@ -13,4 +13,4 @@ def test_talib_bollingerbands_near_zero_values(): {'close': 0.00000014} ]) bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2) - assert (bollinger['upperband'][3] != bollinger['middleband'][3]) + assert bollinger['upperband'][3] != bollinger['middleband'][3] From dadf8adb3ef8897d56dd526b7dd1a2538fbddf35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 20:14:35 +0200 Subject: [PATCH 100/191] Replace filter usage --- freqtrade/rpc/telegram.py | 3 ++- freqtrade/tests/test_plotting.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3eb060074..fe4929780 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -217,7 +217,8 @@ class Telegram(RPC): "*Open Order:* `{open_order}`" if r['open_order'] else "" ] - messages.append("\n".join(filter(None, lines)).format(**r)) + # Filter empty lines using list-comprehension + messages.append("\n".join([l for l in lines if l]).format(**r)) for msg in messages: self._send_msg(msg, bot=bot) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index cef229d19..565659d25 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -23,7 +23,7 @@ def fig_generating_mock(fig, *args, **kwargs): def find_trace_in_fig_data(data, search_string: str): - matches = filter(lambda x: x.name == search_string, data) + matches = (d for d in data if d.name == search_string) return next(matches) From e955b1ae09963d31219926c9334d2e4fabf7d863 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 20:21:57 +0200 Subject: [PATCH 101/191] Use log_has_re instead of plain regex filters for log messages --- freqtrade/tests/test_freqtradebot.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 89d3bba5f..9a22a4f94 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2,7 +2,6 @@ # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments import logging -import re import time from copy import deepcopy from unittest.mock import MagicMock, PropertyMock @@ -1419,8 +1418,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No # Assert we call handle_trade() if trade is feasible for execution freqtrade.update_trade_state(trade) - regexp = re.compile('Found open order for.*') - assert filter(regexp.match, caplog.record_tuples) + assert log_has_re('Found open order for.*', caplog.record_tuples) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker): @@ -1941,14 +1939,11 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - ) Trade.session.add(trade_buy) - regexp = re.compile( - 'Cannot query order for Trade(id=1, pair=ETH/BTC, amount=90.99181073, ' - 'open_rate=0.00001099, open_since=10 hours ago) due to Traceback (most ' - 'recent call last):\n.*' - ) freqtrade.check_handle_timedout() - assert filter(regexp.match, caplog.record_tuples) + assert log_has_re(r'Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, ' + r'open_rate=0.00001099, open_since=10 hours ago\) due to Traceback \(most ' + r'recent call last\):\n.*', caplog.record_tuples) def test_handle_timedout_limit_buy(mocker, default_conf) -> None: From 876cae280709935fa275ccf7a0a20e042596a2dd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jul 2019 22:20:26 +0300 Subject: [PATCH 102/191] docs adjusted to current default values; more detailed description of --eps and --dmmp added --- docs/hyperopt.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index a15fd575a..61e1f9fce 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -204,6 +204,27 @@ Legal values are: - `stoploss`: search for the best stoploss value - space-separated list of any of the above values for example `--spaces roi stoploss` +### Position stacking and disabling max market positions. + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previosly open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is same as setting `max_open_trades` to a very high +number). + +!!! Note + Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. + ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to create a new strategy. @@ -288,19 +309,11 @@ This would translate to the following ROI table: } ``` -### Validate backtest result +### Validate backtesting results Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. -To archive the same results (number of trades, ...) than during hyperopt, please use the command line flags `--disable-max-market-positions` and `--enable-position-stacking` for backtesting. -This configuration is the default in hyperopt for performance reasons. - -You can overwrite position stacking in the configuration by explicitly setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L191). - -Enabling the market-position for hyperopt is currently not possible. - -!!! Note - Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. ## Next Step From 9cae2900d4c301f51032f719c6f7ffee042bb1a6 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 15 Jul 2019 01:44:25 +0300 Subject: [PATCH 103/191] get rid of patched_configuration_open() in tests --- freqtrade/tests/conftest.py | 7 ---- freqtrade/tests/optimize/test_backtesting.py | 14 +++---- freqtrade/tests/optimize/test_edge_cli.py | 8 ++-- freqtrade/tests/optimize/test_hyperopt.py | 7 ++-- freqtrade/tests/test_configuration.py | 43 +++++++++++--------- 5 files changed, 37 insertions(+), 42 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index f57e58774..5862a2e89 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -43,13 +43,6 @@ def get_args(args): return Arguments(args, '').get_parsed_arg() -def patched_configuration_open(mocker, config): - file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( - read_data=json.dumps(config) - )) - return file_mock - - def patched_configuration_load_config_file(mocker, config) -> None: mocker.patch( 'freqtrade.configuration.configuration.Configuration._load_config_file', diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index ee4d23d35..9304871a8 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -22,7 +22,7 @@ from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_open) + patched_configuration_load_config_file) def trim_dictlist(dict_list, num): @@ -165,7 +165,7 @@ def _trend_alternate(dataframe=None, metadata=None): # Unit tests def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -203,7 +203,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x @@ -275,7 +275,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -292,7 +292,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -824,7 +824,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): patch_exchange(mocker, api_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -874,7 +874,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): gen_strattable_mock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', gen_strattable_mock) - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 582271af2..badaa5c45 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -8,11 +8,11 @@ from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_open) + patched_configuration_load_config_file) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -44,7 +44,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: - patched_configuration_open(mocker, edge_conf) + patched_configuration_load_config_file(mocker, edge_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x @@ -91,7 +91,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) - patched_configuration_open(mocker, edge_conf) + patched_configuration_load_config_file(mocker, edge_conf) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 7c00c3023..e8b4aa78d 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -16,8 +16,7 @@ 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 (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file, - patched_configuration_open) + patched_configuration_load_config_file) @pytest.fixture(scope='function') @@ -45,7 +44,7 @@ def create_trials(mocker, hyperopt) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -81,7 +80,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index f6d2deeaa..4f3f4934d 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name - +import json import logging from argparse import Namespace from copy import deepcopy @@ -17,7 +17,8 @@ from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, log_has_re, patched_configuration_open +from freqtrade.tests.conftest import (log_has, log_has_re, + patched_configuration_load_config_file) @pytest.fixture(scope="function") @@ -51,7 +52,9 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None: - file_mock = patched_configuration_open(mocker, default_conf) + file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( + read_data=json.dumps(default_conf) + )) configuration = Configuration(Namespace()) validated_conf = configuration._load_config_file('somefile') @@ -61,7 +64,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = 0 - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -106,7 +109,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = -1 - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -131,7 +134,7 @@ def test_load_config_file_exception(mocker) -> None: def test_load_config(default_conf, mocker) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -143,7 +146,7 @@ def test_load_config(default_conf, mocker) -> None: def test_load_config_with_params(default_conf, mocker) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--dynamic-whitelist', '10', @@ -165,7 +168,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = False conf["db_url"] = "sqlite:///path/to/db.sqlite" - patched_configuration_open(mocker, conf) + patched_configuration_load_config_file(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -181,7 +184,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = "sqlite:///path/to/db.sqlite" - patched_configuration_open(mocker, conf) + patched_configuration_load_config_file(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -197,7 +200,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = False del conf["db_url"] - patched_configuration_open(mocker, conf) + patched_configuration_load_config_file(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -215,7 +218,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = DEFAULT_DB_PROD_URL - patched_configuration_open(mocker, conf) + patched_configuration_load_config_file(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -233,7 +236,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None: 'strategy': 'CustomStrategy', 'strategy_path': '/tmp/strategies', }) - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -244,7 +247,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None: def test_show_info(default_conf, mocker, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--dynamic-whitelist', '10', @@ -268,7 +271,7 @@ def test_show_info(default_conf, mocker, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--config', 'config.json', @@ -307,7 +310,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x @@ -374,7 +377,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non """ Test setup_configuration() function """ - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--config', 'config.json', @@ -422,7 +425,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ 'hyperopt', @@ -497,7 +500,7 @@ def test_check_exchange(default_conf, caplog) -> None: def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) # Prevent setting loggers mocker.patch('freqtrade.loggers._set_loggers', MagicMock) @@ -550,7 +553,7 @@ def test_set_loggers() -> None: def test_set_logfile(default_conf, mocker): - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--logfile', 'test_file.log', @@ -567,7 +570,7 @@ def test_set_logfile(default_conf, mocker): def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: default_conf['forcebuy_enable'] = True - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) From 007703156bc2a7c025e665fcdfbe02bd4aa7fe56 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 15 Jul 2019 01:55:35 +0300 Subject: [PATCH 104/191] do not export ARGS_* from configuration --- freqtrade/configuration/__init__.py | 6 ------ freqtrade/tests/test_arguments.py | 2 +- scripts/download_backtest_data.py | 2 +- scripts/plot_dataframe.py | 3 ++- scripts/plot_profit.py | 3 ++- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 2f74dc597..548b508a7 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,8 +1,2 @@ from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401 - -from freqtrade.configuration.arguments import ( # noqa: F401 - ARGS_DOWNLOADER, - ARGS_PLOT_DATAFRAME, - ARGS_PLOT_PROFIT) - from freqtrade.configuration.configuration import Configuration # noqa: F401 diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index ff1b0e6b0..9de960da3 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -4,7 +4,7 @@ import argparse import pytest from freqtrade.configuration import Arguments, TimeRange -from freqtrade.configuration import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME +from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME from freqtrade.configuration.arguments import check_int_positive diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 60a9e2cbc..9bf05941f 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -9,8 +9,8 @@ from pathlib import Path from typing import Any, Dict, List from freqtrade.configuration import Arguments, TimeRange -from freqtrade.configuration import ARGS_DOWNLOADER from freqtrade.configuration import Configuration +from freqtrade.configuration.arguments import ARGS_DOWNLOADER from freqtrade.configuration.check_exchange import check_exchange from freqtrade.data.history import download_pair_history from freqtrade.exchange import Exchange diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index f0bc30366..fc7e30173 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -18,7 +18,8 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.configuration import Arguments, ARGS_PLOT_DATAFRAME +from freqtrade.configuration import Arguments +from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME from freqtrade.data.btanalysis import extract_trades_of_period from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (init_plotscript, generate_candlestick_graph, diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 632b7879a..96536e1e5 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -8,7 +8,8 @@ import logging import sys from typing import Any, Dict, List -from freqtrade.configuration import Arguments, ARGS_PLOT_PROFIT +from freqtrade.configuration import Arguments +from freqtrade.configuration.arguments import ARGS_PLOT_PROFIT from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import init_plotscript, generate_profit_graph, store_plot_file from freqtrade.state import RunMode From bbab5fef0c83fe44474db15992491d5931f6e7f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 06:27:43 +0200 Subject: [PATCH 105/191] Remove wrong import in legacy startup sript --- bin/freqtrade | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/freqtrade b/bin/freqtrade index b9e3a7008..25c94fe98 100755 --- a/bin/freqtrade +++ b/bin/freqtrade @@ -3,9 +3,7 @@ import sys import warnings -from freqtrade.main import main, set_loggers - -set_loggers() +from freqtrade.main import main warnings.warn( "Deprecated - To continue to run the bot like this, please run `pip install -e .` again.", From a3b7e1f774dad3f5f1448224288d1b294f91d32a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 06:59:20 +0200 Subject: [PATCH 106/191] Update wording in docs --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 61e1f9fce..f0b578814 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -216,7 +216,7 @@ some potential trades to be hidden (or masked) by previosly open trades. The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` -during Hyperopt/Backtesting (which is same as setting `max_open_trades` to a very high +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high number). !!! Note From 107f00ff8f23f2721da9aa41d2a3fa9d71538832 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 20:17:15 +0200 Subject: [PATCH 107/191] Add hyperopt option to clean temporary pickle files --- freqtrade/configuration/arguments.py | 10 +++++++++- freqtrade/configuration/configuration.py | 2 ++ freqtrade/optimize/hyperopt.py | 12 ++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e940ae2a..a3b2ca61f 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -223,6 +223,13 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=1, ), + "hyperopt_clean_state": Arg( + "--clean", + help="Remove temporary hyperopt files (should be used when the custom hyperopt file " + "was changed, or when changing the arguments for --min-trades or spaces.", + default=False, + action='store_true', + ), # List exchanges "print_one_column": Arg( '-1', '--one-column', @@ -309,7 +316,8 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", - "hyperopt_random_state", "hyperopt_min_trades"] + "hyperopt_random_state", "hyperopt_min_trades", + "hyperopt_clean_state"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4959c2adc..ab8d018d5 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -284,6 +284,8 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') + self._args_to_config(config, argname='hyperopt_clean_state', + logstring='Removing hyperopt temp files') return config diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e3683a66c..577180e3b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -63,10 +63,22 @@ class Hyperopt(Backtesting): # Note, this is ratio. 3.85 stated above means 385Σ%. self.expected_max_profit = 3.0 + if self.config['hyperopt_clean_state']: + self.clean_hyperopt() # Previous evaluations self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] + def clean_hyperopt(self): + """ + Remove hyperopt pickle files to restart hyperopt. + """ + for f in [TICKERDATA_PICKLE, TRIALSDATA_PICKLE]: + p = Path(f) + if p.is_file(): + logger.info(f"Removing `{p}`.") + p.unlink() + def get_args(self, params): dimensions = self.hyperopt_space() # Ensure the number of dimensions match From b1b4048f97783a56ecb55de9b1483dec277b63af Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 20:27:34 +0200 Subject: [PATCH 108/191] Add test for hyperopt --- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/tests/optimize/test_hyperopt.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 577180e3b..2bbfd3474 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -63,7 +63,7 @@ class Hyperopt(Backtesting): # Note, this is ratio. 3.85 stated above means 385Σ%. self.expected_max_profit = 3.0 - if self.config['hyperopt_clean_state']: + if self.config.get('hyperopt_clean_state'): self.clean_hyperopt() # Previous evaluations self.trials_file = TRIALSDATA_PICKLE diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index e8b4aa78d..5eb1e02dc 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -11,7 +11,7 @@ from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE +from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE, TICKERDATA_PICKLE from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -510,3 +510,20 @@ def test_generate_optimizer(mocker, default_conf) -> None: hyperopt = Hyperopt(default_conf) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected + + +def test_clean_hyperopt(mocker, default_conf, caplog): + patch_exchange(mocker) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, + }) + mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) + unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) + hyp = Hyperopt(default_conf) + + hyp.clean_hyperopt() + assert unlinkmock.call_count == 2 + assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog.record_tuples) From 2fedae60603073f7853106c3f8d048be09d3b9ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 20:28:55 +0200 Subject: [PATCH 109/191] Move unnecessary things out of generate_optimizer --- freqtrade/optimize/hyperopt.py | 31 ++++++++++++++--------- freqtrade/tests/optimize/test_hyperopt.py | 4 +++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 2bbfd3474..c3550da52 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -69,6 +69,20 @@ class Hyperopt(Backtesting): self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] + # Populate functions here (hasattr is slow so should not be run during "regular" operations) + if hasattr(self.custom_hyperopt, 'populate_buy_trend'): + self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore + + if hasattr(self.custom_hyperopt, 'populate_sell_trend'): + self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore + + # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): + self.max_open_trades = self.config['max_open_trades'] + else: + logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') + self.max_open_trades = 0 + def clean_hyperopt(self): """ Remove hyperopt pickle files to restart hyperopt. @@ -184,39 +198,32 @@ class Hyperopt(Backtesting): return spaces def generate_optimizer(self, _params: Dict) -> Dict: + """ + Used Optimize function. Called once per epoch to optimize whatever is configured. + Keep this function as optimized as possible! + """ params = self.get_args(_params) if self.has_space('roi'): self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) if self.has_space('buy'): self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) - elif hasattr(self.custom_hyperopt, 'populate_buy_trend'): - self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore if self.has_space('sell'): self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) - elif hasattr(self.custom_hyperopt, 'populate_sell_trend'): - self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore if self.has_space('stoploss'): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) - # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set - if self.config.get('use_max_market_positions', True): - max_open_trades = self.config['max_open_trades'] - else: - logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') - max_open_trades = 0 - min_date, max_date = get_timeframe(processed) results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, - 'max_open_trades': max_open_trades, + 'max_open_trades': self.max_open_trades, 'position_stacking': self.config.get('position_stacking', False), 'start_date': min_date, 'end_date': max_date, diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 5eb1e02dc..39f83a0e0 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -371,6 +371,10 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: assert dumper.called # Should be called twice, once for tickerdata, once to save evaluations assert dumper.call_count == 2 + assert hasattr(hyperopt, "advise_sell") + assert hasattr(hyperopt, "advise_buy") + assert hasattr(hyperopt, "max_open_trades") + assert hyperopt.max_open_trades == default_conf['max_open_trades'] def test_format_results(hyperopt): From 8096a1fb04f4993c49ed8d68a1fd57f670aae1ca Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 15 Jul 2019 22:17:57 +0300 Subject: [PATCH 110/191] minor: configuration cleanup --- freqtrade/configuration/configuration.py | 159 +++++++++++++---------- 1 file changed, 89 insertions(+), 70 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4959c2adc..d34434cb2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -40,49 +40,20 @@ class Configuration(object): return self.config - def load_config(self) -> Dict[str, Any]: + def _load_config_files(self) -> Dict[str, Any]: """ - Extract information for sys.argv and load the bot configuration - :return: Configuration dictionary + Iterate through the config files passed in the args, + loading all of them and merging their contents. """ config: Dict[str, Any] = {} - # Now expecting a list of config filenames here, not a string + + # We expect here a list of config filenames 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) - if 'internals' not in config: - config['internals'] = {} - - logger.info('Validating configuration ...') - validate_config_schema(config) - self._validate_config_consistency(config) - - # Set strategy if not specified in config and or if it's non default - if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): - config.update({'strategy': self.args.strategy}) - - if self.args.strategy_path: - config.update({'strategy_path': self.args.strategy_path}) - - # Load Common configuration - config = self._load_common_config(config) - - # 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 - self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE - - config.update({'runmode': self.runmode}) - return config def _load_config_file(self, path: str) -> Dict[str, Any]: @@ -94,13 +65,50 @@ class Configuration(object): try: # Read config from stdin if requested in the options with open(path) if path != '-' else sys.stdin as file: - conf = json.load(file) + config = json.load(file) except FileNotFoundError: raise OperationalException( f'Config file "{path}" not found!' ' Please create a config file or check whether it exists.') - return conf + return config + + def _normalize_config(self, config: Dict[str, Any]) -> None: + """ + Make config more canonical -- i.e. for example add missing parts that we expect + to be normally in it... + """ + if 'internals' not in config: + config['internals'] = {} + + def load_config(self) -> Dict[str, Any]: + """ + Extract information for sys.argv and load the bot configuration + :return: Configuration dictionary + """ + # Load all configs + config: Dict[str, Any] = self._load_config_files() + + # Make resulting config more canonical + self._normalize_config(config) + + logger.info('Validating configuration ...') + validate_config_schema(config) + self._validate_config_consistency(config) + + # Load Common configuration + self._load_common_config(config) + + # Load Optimize configurations + self._load_optimize_config(config) + + # Add plotting options if available + self._load_plot_config(config) + + # Set runmode + self._load_runmode_config(config) + + return config def _load_logging_config(self, config: Dict[str, Any]) -> None: """ @@ -118,16 +126,22 @@ class Configuration(object): setup_logging(config) - def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_strategy_config(self, config: Dict[str, Any]) -> None: + + # Set strategy if not specified in config and or if it's non default + if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): + config.update({'strategy': self.args.strategy}) + + if self.args.strategy_path: + config.update({'strategy_path': self.args.strategy_path}) + + def _load_common_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load common configuration :return: configuration as dictionary """ self._load_logging_config(config) - - # Support for sd_notify - if 'sd_notify' in self.args and self.args.sd_notify: - config['internals'].update({'sd_notify': True}) + self._load_strategy_config(config) # Add dynamic_whitelist if found if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: @@ -157,6 +171,8 @@ class Configuration(object): config['db_url'] = constants.DEFAULT_DB_PROD_URL logger.info('Dry run is disabled') + logger.info(f'Using DB: "{config["db_url"]}"') + if config.get('forcebuy_enable', False): logger.warning('`forcebuy` RPC message enabled.') @@ -164,32 +180,13 @@ class Configuration(object): if config.get('max_open_trades') == -1: config['max_open_trades'] = float('inf') - logger.info(f'Using DB: "{config["db_url"]}"') + # Support for sd_notify + if 'sd_notify' in self.args and self.args.sd_notify: + config['internals'].update({'sd_notify': True}) # Check if the exchange set by the user is supported check_exchange(config) - return config - - def _args_to_config(self, config: Dict[str, Any], argname: str, - logstring: str, logfun: Optional[Callable] = None) -> None: - """ - :param config: Configuration dictionary - :param argname: Argumentname in self.args - will be copied to config dict. - :param logstring: Logging String - :param logfun: logfun is applied to the configuration entry before passing - that entry to the log string using .format(). - sample: logfun=len (prints the length of the found - configuration instead of the content) - """ - if argname in self.args and getattr(self.args, argname): - - config.update({argname: getattr(self.args, argname)}) - if logfun: - logger.info(logstring.format(logfun(config[argname]))) - 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: @@ -201,12 +198,11 @@ class Configuration(object): config.update({'datadir': create_datadir(config, None)}) logger.info('Using data directory: %s ...', config.get('datadir')) - def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_optimize_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load Optimize configuration :return: configuration as dictionary """ - # This will override the strategy configuration self._args_to_config(config, argname='ticker_interval', logstring='Parameter -i/--ticker-interval detected ... ' @@ -285,14 +281,11 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') - return config - - def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_plot_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv Plotting configuration :return: configuration as dictionary """ - self._args_to_config(config, argname='pairs', logstring='Using pairs {}') @@ -306,7 +299,14 @@ class Configuration(object): logstring='Limiting plot to: {}') self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') - return config + + def _load_runmode_config(self, config: Dict[str, Any]) -> None: + if not self.runmode: + # Handle real mode, infer dry/live from config + self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE + logger.info("Runmode set to {self.runmode}.") + + config.update({'runmode': self.runmode}) def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: """ @@ -336,3 +336,22 @@ class Configuration(object): raise OperationalException( f'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive_offset in your config.') + + def _args_to_config(self, config: Dict[str, Any], argname: str, + logstring: str, logfun: Optional[Callable] = None) -> None: + """ + :param config: Configuration dictionary + :param argname: Argumentname in self.args - will be copied to config dict. + :param logstring: Logging String + :param logfun: logfun is applied to the configuration entry before passing + that entry to the log string using .format(). + sample: logfun=len (prints the length of the found + configuration instead of the content) + """ + if argname in self.args and getattr(self.args, argname): + + config.update({argname: getattr(self.args, argname)}) + if logfun: + logger.info(logstring.format(logfun(config[argname]))) + else: + logger.info(logstring.format(config[argname])) From 2a20423be6b10cf1e1c6d38c29ab48a919557007 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 21:35:42 +0200 Subject: [PATCH 111/191] Allow loading custom hyperopt loss functions --- freqtrade/configuration/arguments.py | 10 +++++- freqtrade/configuration/configuration.py | 4 +++ freqtrade/optimize/default_hyperopt.py | 37 +++++++++++++++++++--- freqtrade/optimize/hyperopt.py | 32 ++++++++++--------- freqtrade/optimize/hyperopt_loss.py | 37 ++++++++++++++++++++++ user_data/hyperopts/sample_hyperopt.py | 39 +++++++++++++++++++++--- 6 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss.py diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index a3b2ca61f..3e9629fbb 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -230,6 +230,14 @@ AVAILABLE_CLI_OPTIONS = { default=False, action='store_true', ), + "loss_function": Arg( + '--loss-function', + help='Define the loss-function to use for hyperopt.' + 'Possibilities are `legacy`, and `custom` (providing a custom loss-function).' + 'Default: `%(default)s`.', + choices=['legacy', 'custom'], + default='legacy', + ), # List exchanges "print_one_column": Arg( '-1', '--one-column', @@ -317,7 +325,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", - "hyperopt_clean_state"] + "hyperopt_clean_state", "loss_function"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index ab8d018d5..a8a45653e 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -284,9 +284,13 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') + self._args_to_config(config, argname='hyperopt_clean_state', logstring='Removing hyperopt temp files') + self._args_to_config(config, argname='loss_function', + logstring='Using loss function: {}') + return config def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 7f1cb2435..1c93fcc5d 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -1,16 +1,30 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from functools import reduce +from math import exp +from typing import Any, Callable, Dict, List +from datetime import datetime + import talib.abstract as ta from pandas import DataFrame -from typing import Dict, Any, Callable, List -from functools import reduce - from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -class_name = 'DefaultHyperOpts' +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 class DefaultHyperOpts(IHyperOpt): @@ -19,6 +33,21 @@ class DefaultHyperOpts(IHyperOpt): You can override it with your own hyperopt """ + @staticmethod + def hyperopt_loss_custom(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result + @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index c3550da52..ec1af345e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,7 +7,7 @@ This module contains the hyperopt logic import logging import os import sys -from math import exp + from operator import itemgetter from pathlib import Path from pprint import pprint @@ -22,6 +22,7 @@ from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver +from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy logger = logging.getLogger(__name__) @@ -69,6 +70,20 @@ class Hyperopt(Backtesting): self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] + # Assign loss function + if self.config['loss_function'] == 'legacy': + self.calculate_loss = hyperopt_loss_legacy + elif (self.config['loss_function'] == 'custom' and + hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): + self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom + + # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. + # TODO: Maybe this should just stop hyperopt completely? + if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): + logger.warning("Could not load hyperopt configuration. " + "Falling back to legacy configuration.") + self.calculate_loss = hyperopt_loss_legacy + # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore @@ -160,16 +175,6 @@ class Hyperopt(Backtesting): print('.', end='') sys.stdout.flush() - def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float: - """ - Objective function, returns smaller number for more optimal results - """ - trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / self.expected_max_profit) - duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1) - result = trade_loss + profit_loss + duration_loss - return result - def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration @@ -231,9 +236,7 @@ class Hyperopt(Backtesting): ) result_explanation = self.format_results(results) - total_profit = results.profit_percent.sum() trade_count = len(results.index) - trade_duration = results.trade_duration.mean() # If this evaluation contains too short amount of trades to be # interesting -- consider it as 'bad' (assigned max. loss value) @@ -246,7 +249,8 @@ class Hyperopt(Backtesting): 'result': result_explanation, } - loss = self.calculate_loss(total_profit, trade_count, trade_duration) + loss = self.calculate_loss(results=results, trade_count=trade_count, + min_date=min_date.datetime, max_date=max_date.datetime) return { 'loss': loss, diff --git a/freqtrade/optimize/hyperopt_loss.py b/freqtrade/optimize/hyperopt_loss.py new file mode 100644 index 000000000..d80febed5 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss.py @@ -0,0 +1,37 @@ +from math import exp +from pandas import DataFrame + +# Define some constants: + +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 + + +def hyperopt_loss_legacy(results: DataFrame, trade_count: int, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + This is the legacy algorithm (used until now in freqtrade). + Weights are distributed as follows: + * 0.4 to trade duration + * 0.25: Avoiding trade loss + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 7cb55378e..6428a1843 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -1,17 +1,31 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from functools import reduce +from math import exp +from typing import Any, Callable, Dict, List +from datetime import datetime + +import numpy as np# noqa F401 import talib.abstract as ta from pandas import DataFrame -from typing import Dict, Any, Callable, List -from functools import reduce - -import numpy from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -class_name = 'SampleHyperOpts' +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 # This class is a sample. Feel free to customize it. @@ -28,6 +42,21 @@ class SampleHyperOpts(IHyperOpt): roi_space, generate_roi_table, stoploss_space """ + @staticmethod + def hyperopt_loss_custom(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result + @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) From 710443d2003f36c805dd65b56f6dd35dab5cd82d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 21:36:01 +0200 Subject: [PATCH 112/191] Add documentation for custom hyperopt --- docs/hyperopt.md | 54 +++++++++++++++++++++++++++++----- freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 401bfc3fe..cb344abf3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -31,6 +31,7 @@ Depending on the space you want to optimize, only some of the below are required * Optional but recommended * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used +* Add custom loss-function `hyperopt_loss_custom` (Details below) ### 1. Install a Custom Hyperopt File @@ -150,6 +151,45 @@ The above setup expects to find ADX, RSI and Bollinger Bands in the populated in When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in `hyperopt.py`. +### Using a custom loss function + +To use a custom loss function, make sure that the function `hyperopt_loss_custom` is defined in your custom hyperopt class. +You then need to add the command line parameter `--loss custom` to your hyperopt call so this fuction is being used. + +A sample of this can be found below. + +``` python + @staticmethod + def hyperopt_loss_custom(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result +``` + +Currently, the arguments are: + +* `results`: DataFrame containing the result +* `trade_count`: Amount of trades (identical to `len(results)`) +* `min_date`: Start date of the hyperopting TimeFrame +* `min_date`: End date of the hyperopting TimeFrame + +This function needs to return a floating point number (`float`). The smaller that number, the better is the result. The parameters and balancing for this are up to you. + +!!! Note + This function is called once per iteration - so please make sure to have this as optimized as possible to now slow hyperopt down unnecessarily. + +!!! Note + The last 2 arguments, `*args` and `**kwargs` are not strictly necessary but ensure compatibility for the future, so we can easily enable more parameters once we discover what could be usefull without breaking your custom hyperopt file. + ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. @@ -197,14 +237,14 @@ new buy strategy you have. Legal values are: -- `all`: optimize everything -- `buy`: just search for a new buy strategy -- `sell`: just search for a new sell strategy -- `roi`: just optimize the minimal profit table for your strategy -- `stoploss`: search for the best stoploss value -- space-separated list of any of the above values for example `--spaces roi stoploss` +* `all`: optimize everything +* `buy`: just search for a new buy strategy +* `sell`: just search for a new sell strategy +* `roi`: just optimize the minimal profit table for your strategy +* `stoploss`: search for the best stoploss value +* space-separated list of any of the above values for example `--spaces roi stoploss` -### Position stacking and disabling max market positions. +### Position stacking and disabling max market positions In some situations, you may need to run Hyperopt (and Backtesting) with the `--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ec1af345e..8650c147f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -75,7 +75,7 @@ class Hyperopt(Backtesting): self.calculate_loss = hyperopt_loss_legacy elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): - self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom + self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. # TODO: Maybe this should just stop hyperopt completely? From e5170582de5b777fe0a6513723d4853ccb9141f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 21:54:41 +0200 Subject: [PATCH 113/191] Adapt tests to new loss-function method --- freqtrade/optimize/hyperopt.py | 6 +-- freqtrade/tests/optimize/test_hyperopt.py | 62 ++++++++++++++++------- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8650c147f..cc9d9299c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -18,6 +18,7 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension +from freqtrade import OperationalException from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting @@ -71,18 +72,17 @@ class Hyperopt(Backtesting): self.trials: List = [] # Assign loss function - if self.config['loss_function'] == 'legacy': + if self.config.get('loss_function', 'legacy') == 'legacy': self.calculate_loss = hyperopt_loss_legacy elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. - # TODO: Maybe this should just stop hyperopt completely? if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): logger.warning("Could not load hyperopt configuration. " "Falling back to legacy configuration.") - self.calculate_loss = hyperopt_loss_legacy + raise OperationalException("Could not load hyperopt loss function.") # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 39f83a0e0..889d8cb44 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -2,20 +2,24 @@ import os from datetime import datetime from unittest.mock import MagicMock -from filelock import Timeout import pandas as pd import pytest +from arrow import Arrow +from filelock import Timeout from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file -from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE, TICKERDATA_PICKLE from freqtrade.optimize import setup_configuration, start_hyperopt +from freqtrade.optimize.default_hyperopt import DefaultHyperOpts +from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, + Hyperopt) from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode -from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from freqtrade.strategy.interface import SellType +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) @@ -25,6 +29,21 @@ def hyperopt(default_conf, mocker): return Hyperopt(default_conf) +@pytest.fixture(scope='function') +def hyperopt_results(): + return pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, 0.3], + 'profit_abs': [0.2, 0.4, 0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + # Functions for recurrent object patching def create_trials(mocker, hyperopt) -> None: """ @@ -254,26 +273,33 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: ) -def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: - - correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) - over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) - under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20) +def test_loss_calculation_prefer_correct_trade_count(hyperopt, hyperopt_results) -> None: + correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) + over = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades + 100) + under = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades - 100) assert over > correct assert under > correct -def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None: - shorter = hyperopt.calculate_loss(1, 100, 20) - longer = hyperopt.calculate_loss(1, 100, 30) +def test_loss_calculation_prefer_shorter_trades(hyperopt, hyperopt_results) -> None: + resultsb = hyperopt_results.copy() + resultsb['trade_duration'][1] = 20 + + longer = hyperopt.calculate_loss(hyperopt_results, 100) + shorter = hyperopt.calculate_loss(resultsb, 100) assert shorter < longer -def test_loss_calculation_has_limited_profit(hyperopt) -> None: - correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) - over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) - under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) - assert over == correct +def test_loss_calculation_has_limited_profit(hyperopt, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) + over = hyperopt.calculate_loss(results_over, hyperopt.target_trades) + under = hyperopt.calculate_loss(results_under, hyperopt.target_trades) + assert over < correct assert under > correct @@ -472,7 +498,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: ) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', - MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) ) patch_exchange(mocker) mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) From 55e8092cbf8b5a025f90a08f786e854e321ea1eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 22:52:33 +0200 Subject: [PATCH 114/191] Add sharpe ratio as loss function --- docs/hyperopt.md | 14 ++++++++ freqtrade/configuration/arguments.py | 2 +- freqtrade/optimize/hyperopt.py | 4 ++- freqtrade/optimize/hyperopt_loss.py | 27 ++++++++++++++++ freqtrade/tests/optimize/test_hyperopt.py | 39 ++++++++++++++++------- 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index cb344abf3..b341ec669 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -151,6 +151,20 @@ The above setup expects to find ADX, RSI and Bollinger Bands in the populated in When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in `hyperopt.py`. +## Loss-functions + +Each hyperparameter tuning requires a target. This is usually defined as a function, which get's closer to 0 for increasing values. + +By default, freqtrade uses a loss function we call `legacy` - since it's been with freqtrade since the beginning and optimizes for short trade duration. + +This can be configured by using the `--loss ` argument. +Possible options are: + +* `legacy` - The default option, optimizing for short trades and few losses. +* `sharpe` - using the sharpe-ratio to determine the quality of results +* `custom` - Custom defined loss-function [see next section](#using-a-custom-loss-function) + + ### Using a custom loss function To use a custom loss function, make sure that the function `hyperopt_loss_custom` is defined in your custom hyperopt class. diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e9629fbb..b6deb2451 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -235,7 +235,7 @@ AVAILABLE_CLI_OPTIONS = { help='Define the loss-function to use for hyperopt.' 'Possibilities are `legacy`, and `custom` (providing a custom loss-function).' 'Default: `%(default)s`.', - choices=['legacy', 'custom'], + choices=['legacy', 'sharpe', 'custom'], default='legacy', ), # List exchanges diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cc9d9299c..929debc86 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver -from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy +from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe logger = logging.getLogger(__name__) @@ -74,6 +74,8 @@ class Hyperopt(Backtesting): # Assign loss function if self.config.get('loss_function', 'legacy') == 'legacy': self.calculate_loss = hyperopt_loss_legacy + elif self.config.get('loss_function', 'sharpe') == 'sharpe': + self.calculate_loss = hyperopt_loss_sharpe elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore diff --git a/freqtrade/optimize/hyperopt_loss.py b/freqtrade/optimize/hyperopt_loss.py index d80febed5..20194ecb0 100644 --- a/freqtrade/optimize/hyperopt_loss.py +++ b/freqtrade/optimize/hyperopt_loss.py @@ -1,4 +1,7 @@ +from datetime import datetime from math import exp + +import numpy as np from pandas import DataFrame # Define some constants: @@ -35,3 +38,27 @@ def hyperopt_loss_legacy(results: DataFrame, trade_count: int, duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) result = trade_loss + profit_loss + duration_loss return result + + +def hyperopt_loss_sharpe(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + Using sharpe ratio calculation + """ + total_profit = results.profit_percent + days_period = (max_date - min_date).days + + # adding slippage of 0.1% per trade + total_profit = total_profit - 0.0005 + expected_yearly_return = total_profit.sum() / days_period + + if (np.std(total_profit) != 0.): + sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) + else: + sharp_ratio = 1. + + # print(expected_yearly_return, np.std(total_profit), sharp_ratio) + + # Negate sharp-ratio so lower is better (??) + return -sharp_ratio diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 889d8cb44..88d7de39c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -15,6 +15,7 @@ from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, Hyperopt) +from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType @@ -273,32 +274,48 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: ) -def test_loss_calculation_prefer_correct_trade_count(hyperopt, hyperopt_results) -> None: - correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) - over = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades + 100) - under = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades - 100) +def test_loss_calculation_prefer_correct_trade_count(hyperopt_results) -> None: + correct = hyperopt_loss_legacy(hyperopt_results, 600) + over = hyperopt_loss_legacy(hyperopt_results, 600 + 100) + under = hyperopt_loss_legacy(hyperopt_results, 600 - 100) assert over > correct assert under > correct -def test_loss_calculation_prefer_shorter_trades(hyperopt, hyperopt_results) -> None: +def test_loss_calculation_prefer_shorter_trades(hyperopt_results) -> None: resultsb = hyperopt_results.copy() resultsb['trade_duration'][1] = 20 - longer = hyperopt.calculate_loss(hyperopt_results, 100) - shorter = hyperopt.calculate_loss(resultsb, 100) + longer = hyperopt_loss_legacy(hyperopt_results, 100) + shorter = hyperopt_loss_legacy(resultsb, 100) assert shorter < longer -def test_loss_calculation_has_limited_profit(hyperopt, hyperopt_results) -> None: +def test_loss_calculation_has_limited_profit(hyperopt_results) -> None: results_over = hyperopt_results.copy() results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) - over = hyperopt.calculate_loss(results_over, hyperopt.target_trades) - under = hyperopt.calculate_loss(results_under, hyperopt.target_trades) + correct = hyperopt_loss_legacy(hyperopt_results, 600) + over = hyperopt_loss_legacy(results_over, 600) + under = hyperopt_loss_legacy(results_under, 600) + assert over < correct + assert under > correct + + +def test_sharpe_loss_prefers_higher_profits(hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + correct = hyperopt_loss_sharpe(hyperopt_results, len( + hyperopt_results), datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hyperopt_loss_sharpe(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hyperopt_loss_sharpe(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) assert over < correct assert under > correct From 7be25313a5447b8000c51f07216bb2c29af44dc6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 22:59:28 +0200 Subject: [PATCH 115/191] Add some mypy ignores --- freqtrade/optimize/hyperopt.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 929debc86..bf5b70e14 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -73,9 +73,9 @@ class Hyperopt(Backtesting): # Assign loss function if self.config.get('loss_function', 'legacy') == 'legacy': - self.calculate_loss = hyperopt_loss_legacy + self.calculate_loss = hyperopt_loss_legacy # type: ignore elif self.config.get('loss_function', 'sharpe') == 'sharpe': - self.calculate_loss = hyperopt_loss_sharpe + self.calculate_loss = hyperopt_loss_sharpe # type: ignore elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 88d7de39c..aae3405ad 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -302,7 +302,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_results) -> None: under = hyperopt_loss_legacy(results_under, 600) assert over < correct assert under > correct - + def test_sharpe_loss_prefers_higher_profits(hyperopt_results) -> None: results_over = hyperopt_results.copy() From 07a1c48e8c03a614afde98ba55df250d3a437c9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 23:14:07 +0200 Subject: [PATCH 116/191] Fix wrong intendation for custom-hyperopt check --- freqtrade/optimize/default_hyperopt.py | 17 ----------------- freqtrade/optimize/hyperopt.py | 10 +++++----- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 1c93fcc5d..aa3056fc0 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -1,9 +1,7 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement from functools import reduce -from math import exp from typing import Any, Callable, Dict, List -from datetime import datetime import talib.abstract as ta from pandas import DataFrame @@ -33,21 +31,6 @@ class DefaultHyperOpts(IHyperOpt): You can override it with your own hyperopt """ - @staticmethod - def hyperopt_loss_custom(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: - """ - Objective function, returns smaller number for more optimal results - """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() - - trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) - duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) - result = trade_loss + profit_loss + duration_loss - return result - @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index bf5b70e14..e041554dc 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -80,11 +80,11 @@ class Hyperopt(Backtesting): hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore - # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. - if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): - logger.warning("Could not load hyperopt configuration. " - "Falling back to legacy configuration.") - raise OperationalException("Could not load hyperopt loss function.") + # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. + if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): + logger.warning("Could not load hyperopt configuration. " + "Falling back to legacy configuration.") + raise OperationalException("Could not load hyperopt loss function.") # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): From c4e55d78d5304ba79529373a4159be811667b5a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 05:41:39 +0200 Subject: [PATCH 117/191] reword documentation --- docs/hyperopt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b341ec669..ab7217f2c 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -199,10 +199,10 @@ Currently, the arguments are: This function needs to return a floating point number (`float`). The smaller that number, the better is the result. The parameters and balancing for this are up to you. !!! Note - This function is called once per iteration - so please make sure to have this as optimized as possible to now slow hyperopt down unnecessarily. + This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily. !!! Note - The last 2 arguments, `*args` and `**kwargs` are not strictly necessary but ensure compatibility for the future, so we can easily enable more parameters once we discover what could be usefull without breaking your custom hyperopt file. + Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. ## Execute Hyperopt From 7d62bb8c530278f9b67c4b1e27ca26d00d340ef7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 05:50:27 +0200 Subject: [PATCH 118/191] Revert --clean argument to --continue --- freqtrade/configuration/arguments.py | 10 +++++----- freqtrade/configuration/configuration.py | 4 ++-- freqtrade/optimize/hyperopt.py | 8 ++++++-- freqtrade/tests/optimize/test_hyperopt.py | 21 +++++++++++++++++++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index b6deb2451..f72f785a0 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -223,10 +223,10 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=1, ), - "hyperopt_clean_state": Arg( - "--clean", - help="Remove temporary hyperopt files (should be used when the custom hyperopt file " - "was changed, or when changing the arguments for --min-trades or spaces.", + "hyperopt_continue": Arg( + "--continue", + help="Continue hyperopt from previous runs. " + "By default, temporary files will be removed and hyperopt will start from scratch.", default=False, action='store_true', ), @@ -325,7 +325,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", - "hyperopt_clean_state", "loss_function"] + "hyperopt_continue", "loss_function"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index a8a45653e..cb8f77234 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -285,8 +285,8 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') - self._args_to_config(config, argname='hyperopt_clean_state', - logstring='Removing hyperopt temp files') + self._args_to_config(config, argname='hyperopt_continue', + logstring='Hyperopt continue: {}') self._args_to_config(config, argname='loss_function', logstring='Using loss function: {}') diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e041554dc..fece7d9d8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -65,8 +65,11 @@ class Hyperopt(Backtesting): # Note, this is ratio. 3.85 stated above means 385Σ%. self.expected_max_profit = 3.0 - if self.config.get('hyperopt_clean_state'): + if not self.config.get('hyperopt_continue'): self.clean_hyperopt() + else: + logger.info("Continuing on previous hyperopt results.") + # Previous evaluations self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] @@ -99,6 +102,7 @@ class Hyperopt(Backtesting): else: logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') self.max_open_trades = 0 + self.position_stacking = self.config.get('position_stacking', False), def clean_hyperopt(self): """ @@ -231,7 +235,7 @@ class Hyperopt(Backtesting): 'stake_amount': self.config['stake_amount'], 'processed': processed, 'max_open_trades': self.max_open_trades, - 'position_stacking': self.config.get('position_stacking', False), + 'position_stacking': self.position_stacking, 'start_date': min_date, 'end_date': max_date, } diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index aae3405ad..794cba973 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -418,6 +418,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: assert hasattr(hyperopt, "advise_buy") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == default_conf['max_open_trades'] + assert hasattr(hyperopt, "position_stacking") def test_format_results(hyperopt): @@ -569,8 +570,24 @@ def test_clean_hyperopt(mocker, default_conf, caplog): }) mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) - hyp = Hyperopt(default_conf) + Hyperopt(default_conf) - hyp.clean_hyperopt() assert unlinkmock.call_count == 2 assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog.record_tuples) + + +def test_continue_hyperopt(mocker, default_conf, caplog): + patch_exchange(mocker) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, + 'hyperopt_continue': True + }) + mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) + unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) + Hyperopt(default_conf) + + assert unlinkmock.call_count == 0 + assert log_has(f"Continuing on previous hyperopt results.", caplog.record_tuples) From d23179e25c5bb799f049a4602e99acc87993ba3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 06:27:23 +0200 Subject: [PATCH 119/191] Update hyperopt-loss to use resolver --- docs/hyperopt.md | 39 ++++++----- freqtrade/configuration/arguments.py | 15 ++--- freqtrade/constants.py | 1 + freqtrade/optimize/default_hyperopt.py | 14 ---- freqtrade/optimize/default_hyperopt_loss.py | 51 ++++++++++++++ freqtrade/optimize/hyperopt.py | 22 ++----- freqtrade/optimize/hyperopt_loss.py | 64 ------------------ freqtrade/optimize/hyperopt_loss_interface.py | 25 +++++++ freqtrade/resolvers/hyperopt_resolver.py | 66 ++++++++++++++++++- 9 files changed, 177 insertions(+), 120 deletions(-) create mode 100644 freqtrade/optimize/default_hyperopt_loss.py delete mode 100644 freqtrade/optimize/hyperopt_loss.py create mode 100644 freqtrade/optimize/hyperopt_loss_interface.py diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ab7217f2c..81ecc8ed3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -153,31 +153,40 @@ add it to the `populate_indicators()` method in `hyperopt.py`. ## Loss-functions -Each hyperparameter tuning requires a target. This is usually defined as a function, which get's closer to 0 for increasing values. +Each hyperparameter tuning requires a target. This is usually defined as a loss function, which get's closer to 0 for increasing values. -By default, freqtrade uses a loss function we call `legacy` - since it's been with freqtrade since the beginning and optimizes for short trade duration. - -This can be configured by using the `--loss ` argument. -Possible options are: - -* `legacy` - The default option, optimizing for short trades and few losses. -* `sharpe` - using the sharpe-ratio to determine the quality of results -* `custom` - Custom defined loss-function [see next section](#using-a-custom-loss-function) +FreqTrade uses a default loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. +A different version this can be used by using the `--hyperopt-loss ` argument. +This class should be in it's own file within the `user_data/hyperopts/` directory. ### Using a custom loss function -To use a custom loss function, make sure that the function `hyperopt_loss_custom` is defined in your custom hyperopt class. -You then need to add the command line parameter `--loss custom` to your hyperopt call so this fuction is being used. +To use a custom loss Class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt class. +For the sample below, you then need to add the command line parameter `--hyperoptloss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. -A sample of this can be found below. +A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. ``` python +TARGET_TRADES = 600 +EXPECTED_MAX_PROFIT = 3.0 +MAX_ACCEPTED_TRADE_DURATION = 300 + +class SuperDuperHyperOptLoss(IHyperOptLoss): + """ + Defines the default loss function for hyperopt + """ + @staticmethod - def hyperopt_loss_custom(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: """ - Objective function, returns smaller number for more optimal results + Objective function, returns smaller number for better results + This is the legacy algorithm (used until now in freqtrade). + Weights are distributed as follows: + * 0.4 to trade duration + * 0.25: Avoiding trade loss """ total_profit = results.profit_percent.sum() trade_duration = results.trade_duration.mean() diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index f72f785a0..1c1070507 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -230,13 +230,12 @@ AVAILABLE_CLI_OPTIONS = { default=False, action='store_true', ), - "loss_function": Arg( - '--loss-function', - help='Define the loss-function to use for hyperopt.' - 'Possibilities are `legacy`, and `custom` (providing a custom loss-function).' - 'Default: `%(default)s`.', - choices=['legacy', 'sharpe', 'custom'], - default='legacy', + "hyperopt_loss": Arg( + '--hyperopt-loss-class', + help='Specify hyperopt loss class name. Can generate completely different results, ' + 'since the target for optimization is different. (default: `%(default)s`).', + metavar='NAME', + default=constants.DEFAULT_HYPEROPT_LOSS, ), # List exchanges "print_one_column": Arg( @@ -325,7 +324,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", - "hyperopt_continue", "loss_function"] + "hyperopt_continue", "hyperopt_loss"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 7a487fcc7..9b73adcfe 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -12,6 +12,7 @@ HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_HYPEROPT = 'DefaultHyperOpts' +DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' UNLIMITED_STAKE_AMOUNT = 'unlimited' diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index aa3056fc0..ad76ff786 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -10,20 +10,6 @@ from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -# set TARGET_TRADES to suit your number concurrent trades so its realistic -# to the number of days -TARGET_TRADES = 600 -# This is assumed to be expected avg profit * expected trade count. -# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# self.expected_max_profit = 3.85 -# Check that the reported Σ% values do not exceed this! -# Note, this is ratio. 3.85 stated above means 385Σ%. -EXPECTED_MAX_PROFIT = 3.0 - -# max average trade duration in minutes -# if eval ends with higher value, we consider it a failed eval -MAX_ACCEPTED_TRADE_DURATION = 300 - class DefaultHyperOpts(IHyperOpt): """ diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py new file mode 100644 index 000000000..32bcf4dba --- /dev/null +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -0,0 +1,51 @@ +""" +IHyperOptLoss interface +This module defines the interface for the loss-function for hyperopts +""" + +from math import exp + +from pandas import DataFrame + +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss + +# Define some constants: + +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 + + +class DefaultHyperOptLoss(IHyperOptLoss): + """ + Defines the default loss function for hyperopt + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + This is the legacy algorithm (used until now in freqtrade). + Weights are distributed as follows: + * 0.4 to trade duration + * 0.25: Avoiding trade loss + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index fece7d9d8..39a8f073a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -18,12 +18,10 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade import OperationalException from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver -from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver logger = logging.getLogger(__name__) @@ -48,6 +46,9 @@ class Hyperopt(Backtesting): super().__init__(config) self.custom_hyperopt = HyperOptResolver(self.config).hyperopt + self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss + self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function + # set TARGET_TRADES to suit your number concurrent trades so its realistic # to the number of days self.target_trades = 600 @@ -74,21 +75,6 @@ class Hyperopt(Backtesting): self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] - # Assign loss function - if self.config.get('loss_function', 'legacy') == 'legacy': - self.calculate_loss = hyperopt_loss_legacy # type: ignore - elif self.config.get('loss_function', 'sharpe') == 'sharpe': - self.calculate_loss = hyperopt_loss_sharpe # type: ignore - elif (self.config['loss_function'] == 'custom' and - hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): - self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore - - # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. - if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): - logger.warning("Could not load hyperopt configuration. " - "Falling back to legacy configuration.") - raise OperationalException("Could not load hyperopt loss function.") - # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore diff --git a/freqtrade/optimize/hyperopt_loss.py b/freqtrade/optimize/hyperopt_loss.py deleted file mode 100644 index 20194ecb0..000000000 --- a/freqtrade/optimize/hyperopt_loss.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import datetime -from math import exp - -import numpy as np -from pandas import DataFrame - -# Define some constants: - -# set TARGET_TRADES to suit your number concurrent trades so its realistic -# to the number of days -TARGET_TRADES = 600 -# This is assumed to be expected avg profit * expected trade count. -# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# self.expected_max_profit = 3.85 -# Check that the reported Σ% values do not exceed this! -# Note, this is ratio. 3.85 stated above means 385Σ%. -EXPECTED_MAX_PROFIT = 3.0 - -# max average trade duration in minutes -# if eval ends with higher value, we consider it a failed eval -MAX_ACCEPTED_TRADE_DURATION = 300 - - -def hyperopt_loss_legacy(results: DataFrame, trade_count: int, - *args, **kwargs) -> float: - """ - Objective function, returns smaller number for better results - This is the legacy algorithm (used until now in freqtrade). - Weights are distributed as follows: - * 0.4 to trade duration - * 0.25: Avoiding trade loss - """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() - - trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) - duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) - result = trade_loss + profit_loss + duration_loss - return result - - -def hyperopt_loss_sharpe(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: - """ - Objective function, returns smaller number for more optimal results - Using sharpe ratio calculation - """ - total_profit = results.profit_percent - days_period = (max_date - min_date).days - - # adding slippage of 0.1% per trade - total_profit = total_profit - 0.0005 - expected_yearly_return = total_profit.sum() / days_period - - if (np.std(total_profit) != 0.): - sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) - else: - sharp_ratio = 1. - - # print(expected_yearly_return, np.std(total_profit), sharp_ratio) - - # Negate sharp-ratio so lower is better (??) - return -sharp_ratio diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py new file mode 100644 index 000000000..b11b6e661 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -0,0 +1,25 @@ +""" +IHyperOptLoss interface +This module defines the interface for the loss-function for hyperopts +""" + +from abc import ABC, abstractmethod +from datetime import datetime + +from pandas import DataFrame + + +class IHyperOptLoss(ABC): + """ + Interface for freqtrade hyperopts Loss functions. + Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) + """ + ticker_interval: str + + @staticmethod + @abstractmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + """ diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 30e097f3f..89f384254 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -8,8 +8,9 @@ from pathlib import Path from typing import Optional, Dict from freqtrade import OperationalException -from freqtrade.constants import DEFAULT_HYPEROPT +from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_HYPEROPT_LOSS from freqtrade.optimize.hyperopt_interface import IHyperOpt +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver logger = logging.getLogger(__name__) @@ -77,3 +78,66 @@ class HyperOptResolver(IResolver): f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " "or contains Python code errors." ) + + +class HyperOptLossResolver(IResolver): + """ + This class contains all the logic to load custom hyperopt loss class + """ + + __slots__ = ['hyperoptloss'] + + def __init__(self, config: Optional[Dict] = None) -> None: + """ + Load the custom class from config parameter + :param config: configuration dictionary or None + """ + config = config or {} + + # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt + hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS + self.hyperoptloss = self._load_hyperoptloss( + hyperopt_name, extra_dir=config.get('hyperopt_path')) + + # Assign ticker_interval to be used in hyperopt + self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) + + if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): + raise OperationalException( + f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.") + + def _load_hyperoptloss( + self, hyper_loss_name: str, extra_dir: Optional[str] = None) -> IHyperOptLoss: + """ + Search and loads the specified hyperopt loss class. + :param hyper_loss_name: name of the module to import + :param extra_dir: additional directory to search for the given hyperopt + :return: HyperOptLoss instance or None + """ + current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() + + abs_paths = [ + current_path.parent.parent.joinpath('user_data/hyperopts'), + current_path, + ] + + if extra_dir: + # Add extra hyperopt directory on top of search paths + abs_paths.insert(0, Path(extra_dir)) + + for _path in abs_paths: + try: + (hyperoptloss, module_path) = self._search_object(directory=_path, + object_type=IHyperOptLoss, + object_name=hyper_loss_name) + if hyperoptloss: + logger.info( + f"Using resolved hyperopt {hyper_loss_name} from '{module_path}'...") + return hyperoptloss + except FileNotFoundError: + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + + raise OperationalException( + f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist " + "or contains Python code errors." + ) From ec49b22af32d7ac6662f312dc332f6cbe2dff755 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 06:45:13 +0200 Subject: [PATCH 120/191] Add sharpe ratio hyperopt loss --- freqtrade/configuration/configuration.py | 2 +- freqtrade/optimize/default_hyperopt_loss.py | 5 ++- freqtrade/optimize/hyperopt_loss_sharpe.py | 42 ++++++++++++++++++++ freqtrade/tests/optimize/test_hyperopt.py | 44 +++++++++++---------- 4 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss_sharpe.py diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index cb8f77234..e0bcede7b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -288,7 +288,7 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_continue', logstring='Hyperopt continue: {}') - self._args_to_config(config, argname='loss_function', + self._args_to_config(config, argname='hyperopt_loss', logstring='Using loss function: {}') return config diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 32bcf4dba..60faa9f61 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -1,6 +1,7 @@ """ -IHyperOptLoss interface -This module defines the interface for the loss-function for hyperopts +DefaultHyperOptLoss +This module defines the default HyperoptLoss class which is being used for +Hyperoptimization. """ from math import exp diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py new file mode 100644 index 000000000..5a22a215f --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -0,0 +1,42 @@ +""" +IHyperOptLoss interface +This module defines the interface for the loss-function for hyperopts +""" + +from datetime import datetime + +from pandas import DataFrame +import numpy as np + +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss + + +class SharpeHyperOptLoss(IHyperOptLoss): + """ + Defines the a loss function for hyperopt. + This implementation uses the sharpe ratio calculation. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + Using sharpe ratio calculation + """ + total_profit = results.profit_percent + days_period = (max_date - min_date).days + + # adding slippage of 0.1% per trade + total_profit = total_profit - 0.0005 + expected_yearly_return = total_profit.sum() / days_period + + if (np.std(total_profit) != 0.): + sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) + else: + # Define high (negative) sharpe ratio to be clear that this is NOT optimal. + sharp_ratio = 20. + + # print(expected_yearly_return, np.std(total_profit), sharp_ratio) + return -sharp_ratio diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 794cba973..408112594 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -15,8 +15,7 @@ from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, Hyperopt) -from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_args, log_has, log_has_re, @@ -274,48 +273,53 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: ) -def test_loss_calculation_prefer_correct_trade_count(hyperopt_results) -> None: - correct = hyperopt_loss_legacy(hyperopt_results, 600) - over = hyperopt_loss_legacy(hyperopt_results, 600 + 100) - under = hyperopt_loss_legacy(hyperopt_results, 600 - 100) +def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, 600) + over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100) + under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100) assert over > correct assert under > correct -def test_loss_calculation_prefer_shorter_trades(hyperopt_results) -> None: +def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None: resultsb = hyperopt_results.copy() resultsb['trade_duration'][1] = 20 - longer = hyperopt_loss_legacy(hyperopt_results, 100) - shorter = hyperopt_loss_legacy(resultsb, 100) + hl = HyperOptLossResolver(default_conf).hyperoptloss + longer = hl.hyperopt_loss_function(hyperopt_results, 100) + shorter = hl.hyperopt_loss_function(resultsb, 100) assert shorter < longer -def test_loss_calculation_has_limited_profit(hyperopt_results) -> None: +def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - correct = hyperopt_loss_legacy(hyperopt_results, 600) - over = hyperopt_loss_legacy(results_over, 600) - under = hyperopt_loss_legacy(results_under, 600) + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, 600) + over = hl.hyperopt_loss_function(results_over, 600) + under = hl.hyperopt_loss_function(results_under, 600) assert over < correct assert under > correct -def test_sharpe_loss_prefers_higher_profits(hyperopt_results) -> None: +def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - correct = hyperopt_loss_sharpe(hyperopt_results, len( - hyperopt_results), datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hyperopt_loss_sharpe(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hyperopt_loss_sharpe(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) assert over < correct assert under > correct From 12679da5da530b6e7cfc099afe96e33773821792 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 06:50:25 +0200 Subject: [PATCH 121/191] Add test for hyperoptresolver --- freqtrade/tests/optimize/test_hyperopt.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 408112594..a588bab64 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -13,6 +13,7 @@ from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts +from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, Hyperopt) from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver @@ -185,6 +186,18 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: assert hasattr(x, "ticker_interval") +def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: + + hl = DefaultHyperOptLoss + mocker.patch( + 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', + MagicMock(return_value=hl) + ) + x = HyperOptResolver(default_conf, ).hyperopt + assert hasattr(x, "populate_indicators") + assert hasattr(x, "ticker_interval") + + def test_start(mocker, default_conf, caplog) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, default_conf) From 192d7ad735756f22e1c818f6d67fcab1b02b22d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 06:54:38 +0200 Subject: [PATCH 122/191] Add column description to hyperopt documentation --- docs/hyperopt.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 81ecc8ed3..1fbd59dd4 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -31,7 +31,6 @@ Depending on the space you want to optimize, only some of the below are required * Optional but recommended * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used -* Add custom loss-function `hyperopt_loss_custom` (Details below) ### 1. Install a Custom Hyperopt File @@ -200,7 +199,9 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): Currently, the arguments are: -* `results`: DataFrame containing the result +* `results`: DataFrame containing the result + The following columns are available in results (corresponds to the output of backtesting): + `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame From 1493771087974b0552f5de80a8db172e9c292e59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 19:40:42 +0200 Subject: [PATCH 123/191] improve description --- docs/hyperopt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 1fbd59dd4..4108946c2 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -200,8 +200,8 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): Currently, the arguments are: * `results`: DataFrame containing the result - The following columns are available in results (corresponds to the output of backtesting): - `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` + The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): + `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame From be26ba8f8f7bca1696df1c30ec6e976352bf8969 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 16 Jul 2019 23:00:19 +0300 Subject: [PATCH 124/191] rename _load_*_config() methods to _process_*_options() --- freqtrade/configuration/configuration.py | 51 ++++++++++-------------- scripts/download_backtest_data.py | 4 +- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index d34434cb2..213ecaeaf 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -94,23 +94,20 @@ class Configuration(object): logger.info('Validating configuration ...') validate_config_schema(config) + self._validate_config_consistency(config) - # Load Common configuration - self._load_common_config(config) + self._process_common_options(config) - # Load Optimize configurations - self._load_optimize_config(config) + self._process_optimize_options(config) - # Add plotting options if available - self._load_plot_config(config) + self._process_plot_options(config) - # Set runmode - self._load_runmode_config(config) + self._process_runmode(config) return config - def _load_logging_config(self, config: Dict[str, Any]) -> None: + def _process_logging_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load logging configuration: the -v/--verbose, --logfile options @@ -126,7 +123,7 @@ class Configuration(object): setup_logging(config) - def _load_strategy_config(self, config: Dict[str, Any]) -> None: + def _process_strategy_options(self, config: Dict[str, Any]) -> None: # Set strategy if not specified in config and or if it's non default if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): @@ -135,13 +132,10 @@ class Configuration(object): if self.args.strategy_path: config.update({'strategy_path': self.args.strategy_path}) - def _load_common_config(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv and load common configuration - :return: configuration as dictionary - """ - self._load_logging_config(config) - self._load_strategy_config(config) + def _process_common_options(self, config: Dict[str, Any]) -> None: + + self._process_logging_options(config) + self._process_strategy_options(config) # Add dynamic_whitelist if found if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: @@ -187,7 +181,7 @@ class Configuration(object): # Check if the exchange set by the user is supported check_exchange(config) - def _load_datadir_config(self, config: Dict[str, Any]) -> None: + def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load datadir configuration: the --datadir option @@ -198,11 +192,8 @@ class Configuration(object): config.update({'datadir': create_datadir(config, None)}) logger.info('Using data directory: %s ...', config.get('datadir')) - def _load_optimize_config(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv and load Optimize configuration - :return: configuration as dictionary - """ + def _process_optimize_options(self, config: Dict[str, Any]) -> None: + # This will override the strategy configuration self._args_to_config(config, argname='ticker_interval', logstring='Parameter -i/--ticker-interval detected ... ' @@ -232,7 +223,7 @@ class Configuration(object): self._args_to_config(config, argname='timerange', logstring='Parameter --timerange detected: {} ...') - self._load_datadir_config(config) + self._process_datadir_options(config) self._args_to_config(config, argname='refresh_pairs', logstring='Parameter -r/--refresh-pairs-cached detected ...') @@ -281,11 +272,8 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') - def _load_plot_config(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv Plotting configuration - :return: configuration as dictionary - """ + def _process_plot_options(self, config: Dict[str, Any]) -> None: + self._args_to_config(config, argname='pairs', logstring='Using pairs {}') @@ -300,7 +288,8 @@ class Configuration(object): self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') - def _load_runmode_config(self, config: Dict[str, Any]) -> None: + def _process_runmode(self, config: Dict[str, Any]) -> None: + if not self.runmode: # Handle real mode, infer dry/live from config self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE @@ -314,11 +303,11 @@ class Configuration(object): :param conf: Config in JSON format :return: Returns None if everything is ok, otherwise throw an OperationalException """ - # validating trailing stoploss self._validate_trailing_stoploss(conf) def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None: + # Skip if trailing stoploss is not activated if not conf.get('trailing_stop', False): return diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 9bf05941f..2cee54757 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -74,7 +74,7 @@ else: } timeframes = args.timeframes or ['1m', '5m'] -configuration._load_logging_config(config) +configuration._process_logging_options(config) if args.config and args.exchange: logger.warning("The --exchange option is ignored, " @@ -83,7 +83,7 @@ if args.config and args.exchange: # Check if the exchange set by the user is supported check_exchange(config) -configuration._load_datadir_config(config) +configuration._process_datadir_options(config) dl_path = Path(config['datadir']) From 8ccfc0f31635da57045fcf02b012d17178d7de06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 06:24:40 +0200 Subject: [PATCH 125/191] Remove unused variables --- freqtrade/optimize/hyperopt.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 39a8f073a..3cc6efe12 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -49,23 +49,9 @@ class Hyperopt(Backtesting): self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function - # set TARGET_TRADES to suit your number concurrent trades so its realistic - # to the number of days - self.target_trades = 600 self.total_tries = config.get('epochs', 0) self.current_best_loss = 100 - # max average trade duration in minutes - # if eval ends with higher value, we consider it a failed eval - self.max_accepted_trade_duration = 300 - - # This is assumed to be expected avg profit * expected trade count. - # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, - # self.expected_max_profit = 3.85 - # Check that the reported Σ% values do not exceed this! - # Note, this is ratio. 3.85 stated above means 385Σ%. - self.expected_max_profit = 3.0 - if not self.config.get('hyperopt_continue'): self.clean_hyperopt() else: From 0e500de1a0a7b71cb867cdd4514a9a8937660a72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 06:32:24 +0200 Subject: [PATCH 126/191] Add sample loss and improve docstring --- docs/hyperopt.md | 1 + freqtrade/optimize/default_hyperopt_loss.py | 3 ++- user_data/hyperopts/sample_hyperopt.py | 15 --------------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 4108946c2..74d03b8ab 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -186,6 +186,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): Weights are distributed as follows: * 0.4 to trade duration * 0.25: Avoiding trade loss + * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above """ total_profit = results.profit_percent.sum() trade_duration = results.trade_duration.mean() diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 60faa9f61..58be44ab9 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -37,10 +37,11 @@ class DefaultHyperOptLoss(IHyperOptLoss): *args, **kwargs) -> float: """ Objective function, returns smaller number for better results - This is the legacy algorithm (used until now in freqtrade). + This is the Default algorithm Weights are distributed as follows: * 0.4 to trade duration * 0.25: Avoiding trade loss + * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above """ total_profit = results.profit_percent.sum() trade_duration = results.trade_duration.mean() diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 6428a1843..8650d0a98 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -42,21 +42,6 @@ class SampleHyperOpts(IHyperOpt): roi_space, generate_roi_table, stoploss_space """ - @staticmethod - def hyperopt_loss_custom(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: - """ - Objective function, returns smaller number for more optimal results - """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() - - trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) - duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) - result = trade_loss + profit_loss + duration_loss - return result - @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) From 639a4d5cf724101b918db0a098527efcaf55e88d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 07:14:27 +0200 Subject: [PATCH 127/191] Allow importing interface from hyperopt.py --- docs/hyperopt.md | 6 ++++-- freqtrade/optimize/default_hyperopt_loss.py | 2 +- freqtrade/optimize/hyperopt.py | 2 ++ freqtrade/optimize/hyperopt_loss_sharpe.py | 2 +- user_data/hyperopts/sample_hyperopt.py | 14 -------------- 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 74d03b8ab..6be3d590f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -161,12 +161,14 @@ This class should be in it's own file within the `user_data/hyperopts/` director ### Using a custom loss function -To use a custom loss Class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt class. +To use a custom loss Class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. For the sample below, you then need to add the command line parameter `--hyperoptloss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. -A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. +A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py) ``` python +from freqtrade.optimize.hyperopt import IHyperOptLoss + TARGET_TRADES = 600 EXPECTED_MAX_PROFIT = 3.0 MAX_ACCEPTED_TRADE_DURATION = 300 diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 58be44ab9..2879c4091 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -8,7 +8,7 @@ from math import exp from pandas import DataFrame -from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss +from freqtrade.optimize.hyperopt import IHyperOptLoss # Define some constants: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3cc6efe12..759ceffbe 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -21,6 +21,8 @@ from skopt.space import Dimension from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting +# Import IHyperOptLoss to allow users import from this file +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index 5a22a215f..be1a3d4b4 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -8,7 +8,7 @@ from datetime import datetime from pandas import DataFrame import numpy as np -from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss +from freqtrade.optimize.hyperopt import IHyperOptLoss class SharpeHyperOptLoss(IHyperOptLoss): diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 8650d0a98..a78906cf3 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -13,20 +13,6 @@ from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -# set TARGET_TRADES to suit your number concurrent trades so its realistic -# to the number of days -TARGET_TRADES = 600 -# This is assumed to be expected avg profit * expected trade count. -# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# self.expected_max_profit = 3.85 -# Check that the reported Σ% values do not exceed this! -# Note, this is ratio. 3.85 stated above means 385Σ%. -EXPECTED_MAX_PROFIT = 3.0 - -# max average trade duration in minutes -# if eval ends with higher value, we consider it a failed eval -MAX_ACCEPTED_TRADE_DURATION = 300 - # This class is a sample. Feel free to customize it. class SampleHyperOpts(IHyperOpt): From b8704e12b7db7975d498a778bb519f4ad031ff29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 20:51:44 +0200 Subject: [PATCH 128/191] Add sample hyperopt loss file --- user_data/hyperopts/sample_hyperopt_loss.py | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 user_data/hyperopts/sample_hyperopt_loss.py diff --git a/user_data/hyperopts/sample_hyperopt_loss.py b/user_data/hyperopts/sample_hyperopt_loss.py new file mode 100644 index 000000000..d5102bef5 --- /dev/null +++ b/user_data/hyperopts/sample_hyperopt_loss.py @@ -0,0 +1,47 @@ +from math import exp +from datetime import datetime + +from pandas import DataFrame + +from freqtrade.optimize.hyperopt import IHyperOptLoss + +# Define some constants: + +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 + + +class SampleHyperOptLoss(IHyperOptLoss): + """ + Defines the default loss function for hyperopt + This is intendet to give you some inspiration for your own loss function. + + The Function needs to return a number (float) - which becomes for better backtest results. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result From 49b95fe00865a6b80fb4edd974f468ed6fbbacda Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 20:52:17 +0200 Subject: [PATCH 129/191] use Path.cwd() instead of odd parent.parent.parent structure --- freqtrade/resolvers/hyperopt_resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 89f384254..42e5ff31c 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -55,7 +55,7 @@ class HyperOptResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = [ - current_path.parent.parent.joinpath('user_data/hyperopts'), + Path.cwd().joinpath('user_data/hyperopts'), current_path, ] @@ -117,7 +117,7 @@ class HyperOptLossResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = [ - current_path.parent.parent.joinpath('user_data/hyperopts'), + Path.cwd().joinpath('user_data/hyperopts'), current_path, ] From 545ff6f9f104bec6b4c982c96a80a0c637d7d349 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jul 2019 06:31:44 +0200 Subject: [PATCH 130/191] Fix typo --- user_data/hyperopts/sample_hyperopt_loss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_data/hyperopts/sample_hyperopt_loss.py b/user_data/hyperopts/sample_hyperopt_loss.py index d5102bef5..5a2fb72b6 100644 --- a/user_data/hyperopts/sample_hyperopt_loss.py +++ b/user_data/hyperopts/sample_hyperopt_loss.py @@ -25,7 +25,7 @@ MAX_ACCEPTED_TRADE_DURATION = 300 class SampleHyperOptLoss(IHyperOptLoss): """ Defines the default loss function for hyperopt - This is intendet to give you some inspiration for your own loss function. + This is intended to give you some inspiration for your own loss function. The Function needs to return a number (float) - which becomes for better backtest results. """ From 3e5abd18ca6e95fe387fc8f8368ce310800f6184 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jul 2019 06:56:52 +0200 Subject: [PATCH 131/191] Randomize tests again this used to be enabled, but the plugin changed how it works > From v1.0.0 onwards, this plugin no longer randomises tests by default. --- .travis.yml | 3 +-- requirements-dev.txt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 455f4f037..33a45b280 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ addons: install: - cd build_helpers && ./install_ta-lib.sh; cd .. - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH -- pip install --upgrade pytest-random-order - pip install -r requirements-dev.txt - pip install -e . jobs: @@ -27,7 +26,7 @@ jobs: include: - stage: tests script: - - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ + - pytest --random-order --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ # Allow failure for coveralls - coveralls || true name: pytest diff --git a/requirements-dev.txt b/requirements-dev.txt index c360bc85e..83fec3b8a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,5 +9,6 @@ pytest==5.0.0 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 +pytest-random-order==1.0.4 coveralls==1.8.1 mypy==0.711 From 96564d0dad2504cf36683e558e3f7d29b2c5f016 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 10:45:47 +0300 Subject: [PATCH 132/191] remove deprecated --dynamic-whitelist option --- docs/bot-usage.md | 18 ++---------------- docs/configuration.md | 10 +++++----- docs/deprecated.md | 3 +++ docs/edge.md | 2 +- freqtrade/configuration/arguments.py | 12 +----------- freqtrade/configuration/configuration.py | 13 ------------- freqtrade/tests/test_arguments.py | 15 --------------- freqtrade/tests/test_configuration.py | 11 ----------- 8 files changed, 12 insertions(+), 72 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 85692ae14..48ae299cc 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -7,8 +7,8 @@ This page explains the different parameters of the bot and how to run it. ``` usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] - [-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] - [--db-url PATH] [--sd-notify] + [-s NAME] [--strategy-path PATH] [--db-url PATH] + [--sd-notify] {backtesting,edge,hyperopt} ... Free, open source crypto trading bot @@ -34,9 +34,6 @@ optional arguments: Specify strategy class name (default: DefaultStrategy). --strategy-path PATH Specify additional strategy lookup path. - --dynamic-whitelist [INT] - Dynamically generate and update whitelist based on 24h - BaseVolume (default: 20). DEPRECATED. --db-url PATH Override trades database URL, this is useful if dry_run is enabled or in custom deployments (default: None). @@ -119,17 +116,6 @@ freqtrade --strategy AwesomeStrategy --strategy-path /some/directory This is very simple. Copy paste your strategy file into the directory `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. -### How to use **--dynamic-whitelist**? - -!!! danger "DEPRECATED" - This command line option is deprecated. Please move your configurations using it -to the configurations that utilize the `StaticPairList` or `VolumePairList` methods set -in the configuration file -as outlined [here](configuration/#dynamic-pairlists) - -Description of this deprecated feature was moved to [here](deprecated.md). -Please no longer use it. - ### How to use **--db-url**? When you run the bot in Dry-run mode, per default no transactions are diff --git a/docs/configuration.md b/docs/configuration.md index f46ff14a7..84008be04 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -44,8 +44,8 @@ Mandatory Parameters are marked as **Required**. | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. -| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. -| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. +| `exchange.pair_whitelist` | [] | List of currency to use by the bot. +| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. @@ -53,8 +53,8 @@ Mandatory Parameters are marked as **Required**. | `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). -| `pairlist.method` | StaticPairList | Use Static whitelist. [More information below](#dynamic-pairlists). -| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). +| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. +| `pairlist.config` | None | Additional configuration for dynamic pairlists. | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. @@ -381,7 +381,7 @@ section of the configuration. * It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. * `VolumePairList` * Formerly available as `--dynamic-whitelist []`. This command line -option is deprecated and should no longer be used. +option was removed and can no longer be used. * It selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. * There is a possibility to filter low-value coins that would not allow setting a stop loss diff --git a/docs/deprecated.md b/docs/deprecated.md index b63c8f823..c1582ce31 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -6,6 +6,9 @@ and are no longer supported. Please avoid their usage in your configuration. ### The **--dynamic-whitelist** command line option +This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) +and in freqtrade 2019.7 (master branch). + Per default `--dynamic-whitelist` will retrieve the 20 currencies based on BaseVolume. This value can be changed when you run the script. diff --git a/docs/edge.md b/docs/edge.md index 93c15d330..9047758f4 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -3,7 +3,7 @@ This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. !!! Warning - Edge positioning is not compatible with dynamic whitelist. If enabled, it overrides the dynamic whitelist option. + Edge positioning is not compatible with dynamic (volume-based) whitelist. !!! Note Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation. diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e940ae2a..e2e1c6d0d 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -73,16 +73,6 @@ AVAILABLE_CLI_OPTIONS = { help='Specify additional strategy lookup path.', metavar='PATH', ), - "dynamic_whitelist": Arg( - '--dynamic-whitelist', - help='Dynamically generate and update whitelist ' - 'based on 24h BaseVolume (default: %(const)s). ' - 'DEPRECATED.', - const=constants.DYNAMIC_WHITELIST, - type=int, - metavar='INT', - nargs='?', - ), "db_url": Arg( '--db-url', help=f'Override trades database URL, this is useful in custom deployments ' @@ -299,7 +289,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["dynamic_whitelist", "db_url", "sd_notify"] +ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"] ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount", "refresh_pairs"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 213ecaeaf..311323c36 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -137,19 +137,6 @@ class Configuration(object): self._process_logging_options(config) self._process_strategy_options(config) - # Add dynamic_whitelist if found - if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: - # Update to volumePairList (the previous default) - config['pairlist'] = {'method': 'VolumePairList', - 'config': {'number_assets': self.args.dynamic_whitelist} - } - logger.warning( - 'Parameter --dynamic-whitelist has been deprecated, ' - 'and will be completely replaced by the whitelist dict in the future. ' - 'For now: using dynamically generated whitelist based on VolumePairList. ' - '(not applicable with Backtesting and Hyperopt)' - ) - if ('db_url' in self.args and self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL): config.update({'db_url': self.args.db_url}) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 9de960da3..9f2d02f13 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -86,21 +86,6 @@ def test_parse_args_strategy_path_invalid() -> None: Arguments(['--strategy-path'], '').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 TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200') assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-') diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 4f3f4934d..b8ea92e57 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -149,7 +149,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ - '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', '--strategy-path', '/some/path', '--db-url', 'sqlite:///someurl', @@ -158,8 +157,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: configuration = Configuration(args) validated_conf = configuration.load_config() - assert validated_conf.get('pairlist', {}).get('method') == 'VolumePairList' - assert validated_conf.get('pairlist', {}).get('config').get('number_assets') == 10 assert validated_conf.get('strategy') == 'TestStrategy' assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('db_url') == 'sqlite:///someurl' @@ -250,7 +247,6 @@ def test_show_info(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ - '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', '--db-url', 'sqlite:///tmp/testdb', ] @@ -259,13 +255,6 @@ def test_show_info(default_conf, mocker, caplog) -> None: configuration = Configuration(args) configuration.get_config() - assert log_has( - 'Parameter --dynamic-whitelist has been deprecated, ' - 'and will be completely replaced by the whitelist dict in the future. ' - 'For now: using dynamically generated whitelist based on VolumePairList. ' - '(not applicable with Backtesting and Hyperopt)', - caplog.record_tuples - ) assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples) assert log_has('Dry run is enabled', caplog.record_tuples) From 50d2950e6bba7812bf51c15de1a56b3fe6c27da7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 12:12:34 +0300 Subject: [PATCH 133/191] add -V alias for --version --- docs/bot-usage.md | 2 +- freqtrade/configuration/arguments.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 85692ae14..5426cd7c9 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -23,7 +23,7 @@ optional arguments: -h, --help show this help message and exit -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). --logfile FILE Log to the file specified - --version show program's version number and exit + -V, --version show program's version number and exit -c PATH, --config PATH Specify configuration file (default: None). Multiple --config options may be used. Can be set to '-' to diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e940ae2a..518a85dd8 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -44,7 +44,7 @@ AVAILABLE_CLI_OPTIONS = { metavar='FILE', ), "version": Arg( - '--version', + '-V', '--version', action='version', version=f'%(prog)s {__version__}', ), From 75a0998ed25ac2c30a109c0b9a43a59f41d42da4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 18:08:02 +0300 Subject: [PATCH 134/191] docs: restore link to #dynamic-pairlists. --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 84008be04..c2fe23160 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -53,8 +53,8 @@ Mandatory Parameters are marked as **Required**. | `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). -| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. -| `pairlist.config` | None | Additional configuration for dynamic pairlists. +| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists). +| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. From 43d5ec2d4a47f12456eba6bb3f527f46f46c19a9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 18:15:51 +0300 Subject: [PATCH 135/191] docs: removed historical excursus which can confuse new users --- docs/configuration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c2fe23160..e70ce2bad 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -380,8 +380,6 @@ section of the configuration. * `StaticPairList` * It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. * `VolumePairList` - * Formerly available as `--dynamic-whitelist []`. This command line -option was removed and can no longer be used. * It selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. * There is a possibility to filter low-value coins that would not allow setting a stop loss From 8b4827ad85d695dd85a972f919d0505a82eb1587 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 20:53:29 +0200 Subject: [PATCH 136/191] Convert create_datadir to Pathlib --- freqtrade/configuration/create_datadir.py | 12 +++++++----- freqtrade/resolvers/pairlist_resolver.py | 2 +- freqtrade/tests/test_configuration.py | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/configuration/create_datadir.py b/freqtrade/configuration/create_datadir.py index ecb59bc84..acc3a29ca 100644 --- a/freqtrade/configuration/create_datadir.py +++ b/freqtrade/configuration/create_datadir.py @@ -1,18 +1,20 @@ import logging -import os from typing import Any, Dict, Optional +from pathlib import Path logger = logging.getLogger(__name__) def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str: + + folder = Path(datadir) if datadir else Path('user_data/data') if not datadir: # set datadir exchange_name = config.get('exchange', {}).get('name').lower() - datadir = os.path.join('user_data', 'data', exchange_name) + folder = folder.joinpath(exchange_name) - if not os.path.isdir(datadir): - os.makedirs(datadir) + if not folder.is_dir(): + folder.mkdir(parents=True) logger.info(f'Created data directory: {datadir}') - return datadir + return str(folder) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index ac67ca496..651a7d33f 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -39,7 +39,7 @@ class PairListResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() abs_paths = [ - current_path.parent.parent.joinpath('user_data/pairlist'), + Path.cwd().joinpath('user_data/pairlist'), current_path, ] diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 4f3f4934d..0c5d99195 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -585,11 +585,11 @@ def test_validate_default_conf(default_conf) -> None: def test_create_datadir(mocker, default_conf, caplog) -> None: - mocker.patch('os.path.isdir', MagicMock(return_value=False)) - md = MagicMock() - mocker.patch('os.makedirs', md) + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + create_datadir(default_conf, '/foo/bar') - assert md.call_args[0][0] == "/foo/bar" + assert md.call_args[1]['parents'] is True assert log_has('Created data directory: /foo/bar', caplog.record_tuples) From e01c0ab4d680d72a4141ce5a5e60462027a67f31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jul 2019 20:02:28 +0200 Subject: [PATCH 137/191] Improve doc wording --- docs/bot-usage.md | 40 +++++++++++++++++++--------- docs/hyperopt.md | 34 ++++++++++++----------- freqtrade/configuration/arguments.py | 3 ++- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 85692ae14..aef91189a 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -213,19 +213,22 @@ to find optimal parameter values for your stategy. ``` usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--max_open_trades MAX_OPEN_TRADES] - [--stake_amount STAKE_AMOUNT] [-r] - [--customhyperopt NAME] [--eps] [--dmmp] [-e INT] - [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] - [--print-all] [-j JOBS] + [--max_open_trades INT] + [--stake_amount STAKE_AMOUNT] [-r] + [--customhyperopt NAME] [--eps] [-e INT] + [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] + [--dmmp] [--print-all] [-j JOBS] + [--random-state INT] [--min-trades INT] [--continue] + [--hyperopt-loss-class NAME] optional arguments: -h, --help show this help message and exit -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL - Specify ticker interval (1m, 5m, 30m, 1h, 1d). + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades MAX_OPEN_TRADES + --max_open_trades INT Specify max_open_trades to use. --stake_amount STAKE_AMOUNT Specify stake_amount. @@ -235,18 +238,18 @@ optional arguments: run your optimization commands with up-to-date data. --customhyperopt NAME Specify hyperopt class name (default: - DefaultHyperOpts). + `DefaultHyperOpts`). --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). + -e INT, --epochs INT Specify number of epochs (default: 100). + -s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] + Specify which parameters to hyperopt. Space-separated + list. Default: `all`. --dmmp, --disable-max-market-positions Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number). - -e INT, --epochs INT Specify number of epochs (default: 100). - -s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] - Specify which parameters to hyperopt. Space separate - list. Default: all. --print-all Print all results, not only the best ones. -j JOBS, --job-workers JOBS The number of concurrently running jobs for @@ -254,6 +257,19 @@ optional arguments: (default), all CPUs are used, for -2, all CPUs but one are used, etc. If 1 is given, no parallel computing code is used at all. + --random-state INT Set random state to some positive integer for + reproducible hyperopt results. + --min-trades INT Set minimal desired number of trades for evaluations + in the hyperopt optimization path (default: 1). + --continue Continue hyperopt from previous runs. By default, + temporary files will be removed and hyperopt will + start from scratch. + --hyperopt-loss-class NAME + Specify the class name of the hyperopt loss function + class (IHyperOptLoss). Different functions can + generate completely different results, since the + target for optimization is different. (default: + `DefaultHyperOptLoss`). ``` ## Edge commands diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6be3d590f..5ff5310a3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -144,7 +144,7 @@ it will end with telling you which paramter combination produced the best profit The search for best parameters starts with a few random combinations and then uses a regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination -that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`. +that minimizes the value of the [loss function](#loss-functions). The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. When you want to test an indicator that isn't used by the bot currently, remember to @@ -152,17 +152,19 @@ add it to the `populate_indicators()` method in `hyperopt.py`. ## Loss-functions -Each hyperparameter tuning requires a target. This is usually defined as a loss function, which get's closer to 0 for increasing values. +Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. -FreqTrade uses a default loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. +By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. -A different version this can be used by using the `--hyperopt-loss ` argument. +A different version this can be used by using the `--hyperopt-loss-class ` argument. This class should be in it's own file within the `user_data/hyperopts/` directory. -### Using a custom loss function +Currently, the following loss-functions are builtin: `SharpeHyperOptLoss` and `DefaultHyperOptLoss`. -To use a custom loss Class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. -For the sample below, you then need to add the command line parameter `--hyperoptloss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. +### Creating and using a custom loss function + +To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. +For the sample below, you then need to add the command line parameter `--hyperopt-loss-class SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py) @@ -209,7 +211,7 @@ Currently, the arguments are: * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame -This function needs to return a floating point number (`float`). The smaller that number, the better is the result. The parameters and balancing for this are up to you. +This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. !!! Note This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily. @@ -220,7 +222,7 @@ This function needs to return a floating point number (`float`). The smaller tha ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time you will have the result (more than 30 mins). +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. We strongly recommend to use `screen` or `tmux` to prevent any connection loss. @@ -235,8 +237,11 @@ running at least several thousand evaluations. The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below. +!!! Note + By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`. + !!! Warning - When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file. + When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed. ### Execute Hyperopt with Different Ticker-Data Source @@ -246,12 +251,11 @@ use data from directory `user_data/data`. ### Running Hyperopt with Smaller Testset -Use the `--timerange` argument to change how much of the testset -you want to use. The last N ticks/timeframes will be used. -Example: +Use the `--timerange` argument to change how much of the testset you want to use. +To use one month of data, use the following parameter: ```bash -freqtrade hyperopt --timerange -200 +freqtrade hyperopt --timerange 20180401-20180501 ``` ### Running Hyperopt with Smaller Search Space @@ -319,7 +323,7 @@ method, what those values match to. So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: -``` +``` python (dataframe['rsi'] < 29.0) ``` diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 1c1070507..891bf7d93 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -232,7 +232,8 @@ AVAILABLE_CLI_OPTIONS = { ), "hyperopt_loss": Arg( '--hyperopt-loss-class', - help='Specify hyperopt loss class name. Can generate completely different results, ' + help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' + 'Different functions can generate completely different results, ' 'since the target for optimization is different. (default: `%(default)s`).', metavar='NAME', default=constants.DEFAULT_HYPEROPT_LOSS, From 7af24dc48670fe7c04448efd2d985f15c88c7cdd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 17:55:35 +0300 Subject: [PATCH 138/191] cleanup Arguments: name attrs and methods as non-public --- freqtrade/configuration/arguments.py | 29 ++++++++++++++-------------- freqtrade/tests/test_arguments.py | 14 ++++++-------- scripts/download_backtest_data.py | 8 ++++---- scripts/plot_dataframe.py | 5 ++--- scripts/plot_profit.py | 5 ++--- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 518a85dd8..6103d69ec 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -342,14 +342,15 @@ class Arguments(object): """ Arguments Class. Manage the arguments received by the cli """ - def __init__(self, args: Optional[List[str]], description: str) -> None: + def __init__(self, args: Optional[List[str]], description: str, + no_default_config: bool = False) -> None: self.args = args - self.parsed_arg: Optional[argparse.Namespace] = None + self._parsed_arg: Optional[argparse.Namespace] = None self.parser = argparse.ArgumentParser(description=description) + self._no_default_config = no_default_config def _load_args(self) -> None: - self.build_args(optionlist=ARGS_MAIN) - + self._build_args(optionlist=ARGS_MAIN) self._build_subcommands() def get_parsed_arg(self) -> argparse.Namespace: @@ -357,13 +358,13 @@ class Arguments(object): Return the list of arguments :return: List[str] List of arguments """ - if self.parsed_arg is None: + if self._parsed_arg is None: self._load_args() - self.parsed_arg = self.parse_args() + self._parsed_arg = self._parse_args() - return self.parsed_arg + return self._parsed_arg - def parse_args(self, no_default_config: bool = False) -> argparse.Namespace: + def _parse_args(self) -> argparse.Namespace: """ Parses given arguments and returns an argparse Namespace instance. """ @@ -371,12 +372,12 @@ class Arguments(object): # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) - if not no_default_config and parsed_arg.config is None: + if not self._no_default_config and parsed_arg.config is None: parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg - def build_args(self, optionlist, parser=None): + def _build_args(self, optionlist, parser=None): parser = parser or self.parser for val in optionlist: @@ -396,17 +397,17 @@ class Arguments(object): # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd.set_defaults(func=start_backtesting) - self.build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) + self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd.set_defaults(func=start_edge) - self.build_args(optionlist=ARGS_EDGE, parser=edge_cmd) + self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd.set_defaults(func=start_hyperopt) - self.build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) + self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) # Add list-exchanges subcommand list_exchanges_cmd = subparsers.add_parser( @@ -414,7 +415,7 @@ class Arguments(object): help='Print available exchanges.' ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) - self.build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) + self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) @staticmethod def parse_timerange(text: Optional[str]) -> TimeRange: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 9de960da3..2110913f7 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -51,8 +51,8 @@ def test_parse_args_verbose() -> None: def test_common_scripts_options() -> None: arguments = Arguments(['-p', 'ETH/BTC'], '') - arguments.build_args(ARGS_DOWNLOADER) - args = arguments.parse_args() + arguments._build_args(ARGS_DOWNLOADER) + args = arguments._parse_args() assert args.pairs == 'ETH/BTC' @@ -180,9 +180,8 @@ def test_download_data_options() -> None: '--exchange', 'binance' ] arguments = Arguments(args, '') - arguments.build_args(ARGS_DOWNLOADER) - - args = arguments.parse_args() + arguments._build_args(ARGS_DOWNLOADER) + args = arguments._parse_args() assert args.pairs_file == 'file_with_pairs' assert args.datadir == 'datadir/directory' assert args.days == 30 @@ -197,8 +196,8 @@ def test_plot_dataframe_options() -> None: '-p', 'UNITTEST/BTC', ] arguments = Arguments(args, '') - arguments.build_args(ARGS_PLOT_DATAFRAME) - pargs = arguments.parse_args(True) + arguments._build_args(ARGS_PLOT_DATAFRAME) + pargs = arguments._parse_args() assert pargs.indicators1 == "sma10,sma100" assert pargs.indicators2 == "macd,fastd,fastk" assert pargs.plot_limit == 30 @@ -206,7 +205,6 @@ def test_plot_dataframe_options() -> None: def test_check_int_positive() -> None: - assert check_int_positive("3") == 3 assert check_int_positive("1") == 1 assert check_int_positive("100") == 100 diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 2cee54757..ed96cec71 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -22,12 +22,12 @@ logger = logging.getLogger('download_backtest_data') DEFAULT_DL_PATH = 'user_data/data' -arguments = Arguments(sys.argv[1:], 'Download backtest data') -arguments.build_args(ARGS_DOWNLOADER) - # 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) +arguments = Arguments(sys.argv[1:], 'Download backtest data', + no_default_config=True) +arguments._build_args(optionlist=ARGS_DOWNLOADER) +args = arguments._parse_args() # Use bittrex as default exchange exchange_name = args.exchange or 'bittrex' diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index fc7e30173..034a6f448 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -89,9 +89,8 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph dataframe') - arguments.build_args(optionlist=ARGS_PLOT_DATAFRAME) - - parsed_args = arguments.parse_args() + arguments._build_args(optionlist=ARGS_PLOT_DATAFRAME) + parsed_args = arguments._parse_args() # Load the configuration config = setup_configuration(parsed_args, RunMode.OTHER) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 96536e1e5..4290bca45 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -42,9 +42,8 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph profits') - arguments.build_args(optionlist=ARGS_PLOT_PROFIT) - - parsed_args = arguments.parse_args() + arguments._build_args(optionlist=ARGS_PLOT_PROFIT) + parsed_args = arguments._parse_args() # Load the configuration config = setup_configuration(parsed_args, RunMode.OTHER) From 4a144d1c18367ec006dc8a1d35d54c98a028eebb Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 22:43:36 +0300 Subject: [PATCH 139/191] docs: description for whitelist and blacklist fixed --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e70ce2bad..f8dbbbbbb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -44,8 +44,8 @@ Mandatory Parameters are marked as **Required**. | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. -| `exchange.pair_whitelist` | [] | List of currency to use by the bot. -| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. +| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). +| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. From fa8904978b9b4bd27a4daba95d7136e7b0a31712 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jul 2019 06:31:49 +0200 Subject: [PATCH 140/191] Don't use --hyperopt-loss-class, but --hyperopt-loss instead --- docs/bot-usage.md | 4 ++-- docs/hyperopt.md | 8 ++++---- freqtrade/configuration/arguments.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index aef91189a..ff2e3279c 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -219,7 +219,7 @@ usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--dmmp] [--print-all] [-j JOBS] [--random-state INT] [--min-trades INT] [--continue] - [--hyperopt-loss-class NAME] + [--hyperopt-loss NAME] optional arguments: -h, --help show this help message and exit @@ -264,7 +264,7 @@ optional arguments: --continue Continue hyperopt from previous runs. By default, temporary files will be removed and hyperopt will start from scratch. - --hyperopt-loss-class NAME + --hyperopt-loss NAME Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 5ff5310a3..ef3d28188 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -156,15 +156,15 @@ Each hyperparameter tuning requires a target. This is usually defined as a loss By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. -A different version this can be used by using the `--hyperopt-loss-class ` argument. +A different version this can be used by using the `--hyperopt-loss ` argument. This class should be in it's own file within the `user_data/hyperopts/` directory. -Currently, the following loss-functions are builtin: `SharpeHyperOptLoss` and `DefaultHyperOptLoss`. +Currently, the following loss functions are builtin: `SharpeHyperOptLoss` and `DefaultHyperOptLoss`. ### Creating and using a custom loss function To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. -For the sample below, you then need to add the command line parameter `--hyperopt-loss-class SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. +For the sample below, you then need to add the command line parameter `--hyperopt-loss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py) @@ -252,7 +252,7 @@ use data from directory `user_data/data`. ### Running Hyperopt with Smaller Testset Use the `--timerange` argument to change how much of the testset you want to use. -To use one month of data, use the following parameter: +For example, to use one month of data, pass the following parameter to the hyperopt call: ```bash freqtrade hyperopt --timerange 20180401-20180501 diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 891bf7d93..c9304c15a 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -231,7 +231,7 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), "hyperopt_loss": Arg( - '--hyperopt-loss-class', + '--hyperopt-loss', help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. (default: `%(default)s`).', From 89db5c6bab15352c334e79bf0cba2ab720cd9687 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 14:52:59 +0200 Subject: [PATCH 141/191] Extract strategy-specific stuff from search logic will allow extracting all to IResolver --- freqtrade/resolvers/strategy_resolver.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 4a5604db8..114115d8a 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -155,19 +155,23 @@ class StrategyResolver(IResolver): kwargs={'config': config}) if strategy: logger.info(f"Using resolved strategy {strategy_name} from '{module_path}'...") - strategy._populate_fun_len = len( - getfullargspec(strategy.populate_indicators).args) - strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) - strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) - try: - return import_strategy(strategy, config=config) - except TypeError as e: - logger.warning( - f"Impossible to load strategy '{strategy_name}' from {module_path}. " - f"Error: {e}") + break + except FileNotFoundError: logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + if strategy: + strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) + strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) + strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) + + try: + return import_strategy(strategy, config=config) + except TypeError as e: + logger.warning( + f"Impossible to load strategy '{strategy_name}'. " + f"Error: {e}") + raise OperationalException( f"Impossible to load Strategy '{strategy_name}'. This class does not exist " "or contains Python code errors." From b35efd96dc01eb0e1b436577c778b59688ed4d04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:03:12 +0200 Subject: [PATCH 142/191] Extract load_object from multiple paths to iResolver --- freqtrade/resolvers/hyperopt_resolver.py | 34 +++++++++--------------- freqtrade/resolvers/iresolver.py | 25 ++++++++++++++++- freqtrade/resolvers/pairlist_resolver.py | 18 +++++-------- freqtrade/resolvers/strategy_resolver.py | 17 +++--------- 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 42e5ff31c..3f8d03fd2 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -63,17 +63,12 @@ class HyperOptResolver(IResolver): # Add extra hyperopt directory on top of search paths abs_paths.insert(0, Path(extra_dir)) - for _path in abs_paths: - try: - (hyperopt, module_path) = self._search_object(directory=_path, - object_type=IHyperOpt, - object_name=hyperopt_name) - if hyperopt: - logger.info(f"Using resolved hyperopt {hyperopt_name} from '{module_path}'...") - return hyperopt - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - + (hyperopt, module_path) = self._load_object(paths=abs_paths, + object_type=IHyperOpt, + object_name=hyperopt_name, + kwargs={}) + if hyperopt: + return hyperopt raise OperationalException( f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " "or contains Python code errors." @@ -125,17 +120,12 @@ class HyperOptLossResolver(IResolver): # Add extra hyperopt directory on top of search paths abs_paths.insert(0, Path(extra_dir)) - for _path in abs_paths: - try: - (hyperoptloss, module_path) = self._search_object(directory=_path, - object_type=IHyperOptLoss, - object_name=hyper_loss_name) - if hyperoptloss: - logger.info( - f"Using resolved hyperopt {hyper_loss_name} from '{module_path}'...") - return hyperoptloss - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + (hyperoptloss, module_path) = self._load_object(paths=abs_paths, + object_type=IHyperOptLoss, + object_name=hyper_loss_name, + kwargs={}) + if hyperoptloss: + return hyperoptloss raise OperationalException( f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist " diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 1065abba7..aafc4b0dd 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Any, Optional, Tuple, Type, Union +from typing import Any, List, Optional, Tuple, Type, Union logger = logging.getLogger(__name__) @@ -64,3 +64,26 @@ class IResolver(object): if obj: return (obj(**kwargs), module_path) return (None, None) + + @staticmethod + def _load_object(paths: List[Path], object_type, object_name: str, + kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: + """ + Try to load object from path list. + """ + + for _path in paths: + try: + (module, module_path) = IResolver._search_object(directory=_path, + object_type=object_type, + object_name=object_name, + kwargs=kwargs) + if module: + logger.info( + f"Using resolved {object_type.__name__.lower()[1:]} {object_name} " + f"from '{module_path}'...") + return (module, module_path) + except FileNotFoundError: + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + + return (None, None) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 651a7d33f..a74ce1cf7 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -43,18 +43,12 @@ class PairListResolver(IResolver): current_path, ] - for _path in abs_paths: - try: - (pairlist, module_path) = self._search_object(directory=_path, - object_type=IPairList, - object_name=pairlist_name, - kwargs=kwargs) - if pairlist: - logger.info(f"Using resolved pairlist {pairlist_name} from '{module_path}'...") - return pairlist - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - + (pairlist, module_path) = self._load_object(paths=abs_paths, + object_type=IPairList, + object_name=pairlist_name, + kwargs=kwargs) + if pairlist: + return pairlist raise OperationalException( f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist " "or contains Python code errors." diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 114115d8a..ac053399e 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -147,19 +147,10 @@ class StrategyResolver(IResolver): # register temp path with the bot abs_paths.insert(0, temp.resolve()) - for _path in abs_paths: - try: - (strategy, module_path) = self._search_object(directory=_path, - object_type=IStrategy, - object_name=strategy_name, - kwargs={'config': config}) - if strategy: - logger.info(f"Using resolved strategy {strategy_name} from '{module_path}'...") - break - - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - + (strategy, module_path) = self._load_object(paths=abs_paths, + object_type=IStrategy, + object_name=strategy_name, + kwargs={'config': config}) if strategy: strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) From 88eb93da52421ffa74ab5c5a6ad16aceb90b72cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:16:19 +0200 Subject: [PATCH 143/191] Fix base64 strategy test to make sure strategy was loaded via base64 --- freqtrade/tests/strategy/test_strategy.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 9a2c950e5..02a3769fc 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 import logging +import tempfile import warnings from base64 import urlsafe_b64encode from os import path @@ -68,11 +69,15 @@ def test_load_strategy(result): assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) -def test_load_strategy_base64(result): - with open("freqtrade/tests/strategy/test_strategy.py", "rb") as file: +def test_load_strategy_base64(result, caplog): + with open("user_data/strategies/test_strategy.py", "rb") as file: encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + # Make sure strategy was loaded from base64 (using temp directory)!! + assert log_has_re(r"Using resolved strategy TestStrategy from '" + + tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.", + caplog.record_tuples) def test_load_strategy_invalid_directory(result, caplog): From 08ca260e82473f3630ce4dacb8f49fbef839b275 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:25:48 +0200 Subject: [PATCH 144/191] Simplify return valuef rom _load_object --- freqtrade/resolvers/hyperopt_resolver.py | 12 ++++-------- freqtrade/resolvers/iresolver.py | 6 +++--- freqtrade/resolvers/pairlist_resolver.py | 6 ++---- freqtrade/resolvers/strategy_resolver.py | 6 ++---- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 3f8d03fd2..3f39bd41d 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -63,10 +63,8 @@ class HyperOptResolver(IResolver): # Add extra hyperopt directory on top of search paths abs_paths.insert(0, Path(extra_dir)) - (hyperopt, module_path) = self._load_object(paths=abs_paths, - object_type=IHyperOpt, - object_name=hyperopt_name, - kwargs={}) + hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, + object_name=hyperopt_name) if hyperopt: return hyperopt raise OperationalException( @@ -120,10 +118,8 @@ class HyperOptLossResolver(IResolver): # Add extra hyperopt directory on top of search paths abs_paths.insert(0, Path(extra_dir)) - (hyperoptloss, module_path) = self._load_object(paths=abs_paths, - object_type=IHyperOptLoss, - object_name=hyper_loss_name, - kwargs={}) + hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, + object_name=hyper_loss_name) if hyperoptloss: return hyperoptloss diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index aafc4b0dd..192a38beb 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -67,7 +67,7 @@ class IResolver(object): @staticmethod def _load_object(paths: List[Path], object_type, object_name: str, - kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: + kwargs: dict = {}) -> Union[Any, None]: """ Try to load object from path list. """ @@ -82,8 +82,8 @@ class IResolver(object): logger.info( f"Using resolved {object_type.__name__.lower()[1:]} {object_name} " f"from '{module_path}'...") - return (module, module_path) + return module except FileNotFoundError: logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - return (None, None) + return None diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index a74ce1cf7..3d95c0295 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -43,10 +43,8 @@ class PairListResolver(IResolver): current_path, ] - (pairlist, module_path) = self._load_object(paths=abs_paths, - object_type=IPairList, - object_name=pairlist_name, - kwargs=kwargs) + pairlist = self._load_object(paths=abs_paths, object_type=IPairList, + object_name=pairlist_name, kwargs=kwargs) if pairlist: return pairlist raise OperationalException( diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index ac053399e..aa73327ff 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -147,10 +147,8 @@ class StrategyResolver(IResolver): # register temp path with the bot abs_paths.insert(0, temp.resolve()) - (strategy, module_path) = self._load_object(paths=abs_paths, - object_type=IStrategy, - object_name=strategy_name, - kwargs={'config': config}) + strategy = self._load_object(paths=abs_paths, object_type=IStrategy, + object_name=strategy_name, kwargs={'config': config}) if strategy: strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) From e6528be63d3204c1a7c16fe15cff517d820419bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 16:20:45 +0200 Subject: [PATCH 145/191] Config is not optional for hyperopt resolver --- freqtrade/resolvers/hyperopt_resolver.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 3f39bd41d..944687ce7 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -23,12 +23,11 @@ class HyperOptResolver(IResolver): __slots__ = ['hyperopt'] - def __init__(self, config: Optional[Dict] = None) -> None: + def __init__(self, config: Dict) -> None: """ Load the custom class from config parameter :param config: configuration dictionary or None """ - config = config or {} # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT From dcddfce5bcb297d197511659249fd8bff61056a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 19:21:50 +0200 Subject: [PATCH 146/191] Fix small mistakes --- freqtrade/resolvers/hyperopt_resolver.py | 2 +- freqtrade/resolvers/iresolver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 944687ce7..74412e738 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -26,7 +26,7 @@ class HyperOptResolver(IResolver): def __init__(self, config: Dict) -> None: """ Load the custom class from config parameter - :param config: configuration dictionary or None + :param config: configuration dictionary """ # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 192a38beb..9d0c97917 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -67,7 +67,7 @@ class IResolver(object): @staticmethod def _load_object(paths: List[Path], object_type, object_name: str, - kwargs: dict = {}) -> Union[Any, None]: + kwargs: dict = {}) -> Optional[Any]: """ Try to load object from path list. """ From 1fea6d394a6cee4227c678485da1776ea38858bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 19:31:50 +0200 Subject: [PATCH 147/191] Import DefaultStrategy from the correct file --- freqtrade/strategy/__init__.py | 2 -- freqtrade/tests/strategy/test_strategy.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index c62bfe5dc..19eacda42 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,8 +3,6 @@ import sys from copy import deepcopy from freqtrade.strategy.interface import IStrategy -# Import Default-Strategy to have hyperopt correctly resolve -from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401 logger = logging.getLogger(__name__) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 02a3769fc..609cc58ff 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -45,7 +45,7 @@ def test_import_strategy(caplog): def test_search_strategy(): default_config = {} - default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() + default_location = Path(__file__).parent.parent.parent.joinpath('strategy').resolve() s, _ = StrategyResolver._search_object( directory=default_location, From d2ad32eef8e450222d91ad680b5943c45002f314 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 19:56:43 +0200 Subject: [PATCH 148/191] partially revert last commit(DefaultStrategy import IS needed). * don't run functions in travis in a way we don't support --- .travis.yml | 4 ++-- freqtrade/strategy/__init__.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 33a45b280..b44fef7a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,11 +32,11 @@ jobs: name: pytest - script: - cp config.json.example config.json - - python freqtrade --datadir freqtrade/tests/testdata backtesting + - freqtrade --datadir freqtrade/tests/testdata backtesting name: backtest - script: - cp config.json.example config.json - - python freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 + - freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 name: hyperopt - script: flake8 freqtrade scripts name: flake8 diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 19eacda42..c62bfe5dc 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,6 +3,8 @@ import sys from copy import deepcopy from freqtrade.strategy.interface import IStrategy +# Import Default-Strategy to have hyperopt correctly resolve +from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401 logger = logging.getLogger(__name__) From a213674a98bf9ae48ac4ef0c527e9f00340049d1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:05 +0000 Subject: [PATCH 149/191] Update pandas from 0.24.2 to 0.25.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 52442fb19..fef8f06f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ -r requirements-common.txt numpy==1.16.4 -pandas==0.24.2 +pandas==0.25.0 scipy==1.3.0 From d6b6e59ab8d92a8f388ea556831e829821893a47 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:06 +0000 Subject: [PATCH 150/191] Update ccxt from 1.18.860 to 1.18.965 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 409c979b5..5f549e245 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.860 +ccxt==1.18.965 SQLAlchemy==1.3.5 python-telegram-bot==11.1.0 arrow==0.14.2 From 7add015a758a69a0434c36589c99c18dbe17b14a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:07 +0000 Subject: [PATCH 151/191] Update sqlalchemy from 1.3.5 to 1.3.6 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 5f549e245..4c8bab90d 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.18.965 -SQLAlchemy==1.3.5 +SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.2 cachetools==3.1.1 From 6c41ca4b8cb00984c111d6a987072d5cf3f21219 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:08 +0000 Subject: [PATCH 152/191] Update flask from 1.0.3 to 1.1.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 4c8bab90d..3cf34ad61 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -29,4 +29,4 @@ python-rapidjson==0.7.2 sdnotify==0.3.2 # Api server -flask==1.0.3 +flask==1.1.1 From e0cd34c9e1d32e7017acb22fdac6bdfc763cb432 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:09 +0000 Subject: [PATCH 153/191] Update flake8 from 3.7.7 to 3.7.8 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 83fec3b8a..28fd19ce4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ -r requirements.txt -r requirements-plot.txt -flake8==3.7.7 +flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 pytest==5.0.0 From bd0faaf70291b7f8e8b1fb4fe9d42197fd2102c3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:11 +0000 Subject: [PATCH 154/191] Update pytest from 5.0.0 to 5.0.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 28fd19ce4..e6f7bde1c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==5.0.0 +pytest==5.0.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From 76b9d781ee9b028b12e78409444b6f34ab0f1a41 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:12 +0000 Subject: [PATCH 155/191] Update mypy from 0.711 to 0.720 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e6f7bde1c..946d63039 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,4 +11,4 @@ pytest-asyncio==0.10.0 pytest-cov==2.7.1 pytest-random-order==1.0.4 coveralls==1.8.1 -mypy==0.711 +mypy==0.720 From 44b2261c347eb4757e3a6976e340e7e23f8c34dd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:13 +0000 Subject: [PATCH 156/191] Update plotly from 3.10.0 to 4.0.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index d4e4fc165..a6753fc3f 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.10.0 +plotly==4.0.0 From 04382d4b44c867034ff4c1dd21e49d432879baf3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 22 Jul 2019 19:37:34 +0300 Subject: [PATCH 157/191] add --hyperopt-path option --- docs/bot-usage.md | 5 ++++- freqtrade/configuration/arguments.py | 8 +++++++- freqtrade/configuration/configuration.py | 7 +++++-- freqtrade/resolvers/hyperopt_resolver.py | 4 ++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index c643ecace..8031a2072 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -201,7 +201,8 @@ to find optimal parameter values for your stategy. usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] [-r] - [--customhyperopt NAME] [--eps] [-e INT] + [--customhyperopt NAME] [--hyperopt-path PATH] + [--eps] [-e INT] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--dmmp] [--print-all] [-j JOBS] [--random-state INT] [--min-trades INT] [--continue] @@ -225,6 +226,8 @@ optional arguments: --customhyperopt NAME Specify hyperopt class name (default: `DefaultHyperOpts`). + --hyperopt-path PATH Specify additional lookup path for Hyperopts and + Hyperopt Loss functions. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3bbdc4bc2..c70d1b4d2 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -168,6 +168,11 @@ AVAILABLE_CLI_OPTIONS = { metavar='NAME', default=constants.DEFAULT_HYPEROPT, ), + "hyperopt_path": Arg( + '--hyperopt-path', + help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', + metavar='PATH', + ), "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', @@ -312,7 +317,8 @@ ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "live", "strategy_list", "export", "exportfilename"] -ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", +ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", + "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", "hyperopt_continue", "hyperopt_loss"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 737eb0900..c97bd277a 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -129,8 +129,8 @@ class Configuration(object): if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): config.update({'strategy': self.args.strategy}) - if self.args.strategy_path: - config.update({'strategy_path': self.args.strategy_path}) + self._args_to_config(config, argname='strategy_path', + logstring='Using additional Strategy lookup path: {}') def _process_common_options(self, config: Dict[str, Any]) -> None: @@ -239,6 +239,9 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt', logstring='Using Hyperopt file {}') + self._args_to_config(config, argname='hyperopt_path', + logstring='Using additional Hyperopt lookup path: {}') + self._args_to_config(config, argname='epochs', logstring='Parameter --epochs detected ... ' 'Will run Hyperopt with for {} epochs ...' diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 74412e738..5027d7ddf 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -60,7 +60,7 @@ class HyperOptResolver(IResolver): if extra_dir: # Add extra hyperopt directory on top of search paths - abs_paths.insert(0, Path(extra_dir)) + abs_paths.insert(0, Path(extra_dir).resolve()) hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, object_name=hyperopt_name) @@ -115,7 +115,7 @@ class HyperOptLossResolver(IResolver): if extra_dir: # Add extra hyperopt directory on top of search paths - abs_paths.insert(0, Path(extra_dir)) + abs_paths.insert(0, Path(extra_dir).resolve()) hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, object_name=hyper_loss_name) From 482f5f7a26c95a2d215e0b98edb817b169317d96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Jul 2019 20:39:38 +0200 Subject: [PATCH 158/191] Update plotly dependencies (will break 3.x installations) --- freqtrade/plot/plotting.py | 36 ++++++++++++++++---------------- freqtrade/resolvers/iresolver.py | 2 +- freqtrade/tests/test_plotting.py | 6 +++--- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index dde6f78f0..5c9c6e457 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -15,9 +15,9 @@ logger = logging.getLogger(__name__) try: - from plotly import tools + from plotly.subplots import make_subplots from plotly.offline import plot - import plotly.graph_objs as go + import plotly.graph_objects as go except ImportError: logger.exception("Module plotly not found \n Please install using `pip install plotly`") exit(1) @@ -62,7 +62,7 @@ def init_plotscript(config): } -def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: +def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots: """ Generator all the indicator selected by the user for a specific row :param fig: Plot figure to append to @@ -79,7 +79,7 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools mode='lines', name=indicator ) - fig.append_trace(scattergl, row, 1) + fig.add_trace(scattergl, row, 1) else: logger.info( 'Indicator "%s" ignored. Reason: This indicator is not found ' @@ -90,7 +90,7 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools return fig -def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> tools.make_subplots: +def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots: """ Add profit-plot :param fig: Plot figure to append to @@ -105,12 +105,12 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> tools.ma y=data[column], name=name, ) - fig.append_trace(profit, row, 1) + fig.add_trace(profit, row, 1) return fig -def plot_trades(fig, trades: pd.DataFrame) -> tools.make_subplots: +def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: """ Add trades to "fig" """ @@ -145,8 +145,8 @@ def plot_trades(fig, trades: pd.DataFrame) -> tools.make_subplots: color='red' ) ) - fig.append_trace(trade_buys, 1, 1) - fig.append_trace(trade_sells, 1, 1) + fig.add_trace(trade_buys, 1, 1) + fig.add_trace(trade_sells, 1, 1) else: logger.warning("No trades found.") return fig @@ -167,7 +167,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra """ # Define the graph - fig = tools.make_subplots( + fig = make_subplots( rows=3, cols=1, shared_xaxes=True, @@ -189,7 +189,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra close=data.close, name='Price' ) - fig.append_trace(candles, 1, 1) + fig.add_trace(candles, 1, 1) if 'buy' in data.columns: df_buy = data[data['buy'] == 1] @@ -206,7 +206,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra color='green', ) ) - fig.append_trace(buys, 1, 1) + fig.add_trace(buys, 1, 1) else: logger.warning("No buy-signals found.") @@ -225,7 +225,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra color='red', ) ) - fig.append_trace(sells, 1, 1) + fig.add_trace(sells, 1, 1) else: logger.warning("No sell-signals found.") @@ -244,8 +244,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra 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.add_trace(bb_lower, 1, 1) + fig.add_trace(bb_upper, 1, 1) # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data) @@ -258,7 +258,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra y=data['volume'], name='Volume' ) - fig.append_trace(volume, 2, 1) + fig.add_trace(volume, 2, 1) # Add indicators to seperate row fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data) @@ -281,10 +281,10 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], name='Avg close price', ) - fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) + fig = make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) fig['layout'].update(title="Profit plot") - fig.append_trace(avgclose, 1, 1) + fig.add_trace(avgclose, 1, 1) fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') for pair in pairs: diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 9d0c97917..841c3cf43 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -84,6 +84,6 @@ class IResolver(object): f"from '{module_path}'...") return module except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.resolve()) return None diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 0ebadf720..509bf7880 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -2,8 +2,8 @@ from copy import deepcopy from unittest.mock import MagicMock -import plotly.graph_objs as go -from plotly import tools +import plotly.graph_objects as go +from plotly.subplots import make_subplots from freqtrade.configuration import Arguments, TimeRange from freqtrade.data import history @@ -28,7 +28,7 @@ def find_trace_in_fig_data(data, search_string: str): def generage_empty_figure(): - return tools.make_subplots( + return make_subplots( rows=3, cols=1, shared_xaxes=True, From 60cf56e23582fd7a0e06db0ce080640670cef836 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Jul 2019 20:57:40 +0200 Subject: [PATCH 159/191] Adapt tests to always provide message for ccxt exceptions Changes introduced in https://github.com/ccxt/ccxt/pull/5470 --- freqtrade/tests/exchange/test_exchange.py | 54 +++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index a4f1bca18..a5cdf0a82 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -33,13 +33,13 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, **kwargs): with pytest.raises(TemporaryError): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 @@ -47,13 +47,13 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): with pytest.raises(TemporaryError): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 @@ -256,13 +256,13 @@ def test__load_async_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() - api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError()) + api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) Exchange(default_conf) - assert log_has('Unable to initialize markets. Reason: ', caplog.record_tuples) + assert log_has('Unable to initialize markets. Reason: SomeError', caplog.record_tuples) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() @@ -305,7 +305,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) api_mock = MagicMock() - api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError) + api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError")) default_conf['exchange']['markets_refresh_interval'] = 10 exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") @@ -634,25 +634,25 @@ def test_buy_prod(default_conf, mocker, exchange_name): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) @@ -758,22 +758,22 @@ def test_sell_prod(default_conf, mocker, exchange_name): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) @@ -846,7 +846,7 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): assert exchange.get_balance(currency='BTC') == 123.4 with pytest.raises(OperationalException): - api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_balance(currency='BTC') @@ -919,7 +919,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): "get_tickers", "fetch_tickers") with pytest.raises(OperationalException): - api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported) + api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() @@ -1101,7 +1101,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ api_mock = MagicMock() with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): - api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", (arrow.utcnow().timestamp - 2000) * 1000) @@ -1173,15 +1173,15 @@ def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name): def test_get_order_book_exception(default_conf, mocker, exchange_name): api_mock = MagicMock() with pytest.raises(OperationalException): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) with pytest.raises(TemporaryError): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) with pytest.raises(OperationalException): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) @@ -1294,7 +1294,7 @@ def test_cancel_order(default_conf, mocker, exchange_name): assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 with pytest.raises(InvalidOrderException): - api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.cancel_order(order_id='_', pair='TKN/BTC') assert api_mock.cancel_order.call_count == 1 @@ -1321,7 +1321,7 @@ def test_get_order(default_conf, mocker, exchange_name): assert exchange.get_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): - api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 @@ -1437,22 +1437,22 @@ def test_stoploss_limit_order(default_conf, mocker): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) From 0c2c094db6233383079c54b42eae296daf4fb5d9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 23 Jul 2019 18:51:24 +0300 Subject: [PATCH 160/191] minor: add OnlyProfitHyperOptLoss --- freqtrade/optimize/default_hyperopt_loss.py | 11 +++--- .../optimize/hyperopt_loss_onlyprofit.py | 34 +++++++++++++++++++ freqtrade/optimize/hyperopt_loss_sharpe.py | 17 ++++++---- 3 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss_onlyprofit.py diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 2879c4091..4ab9fbe44 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -3,27 +3,26 @@ DefaultHyperOptLoss This module defines the default HyperoptLoss class which is being used for Hyperoptimization. """ - from math import exp from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss -# Define some constants: -# set TARGET_TRADES to suit your number concurrent trades so its realistic +# Set TARGET_TRADES to suit your number concurrent trades so its realistic # to the number of days TARGET_TRADES = 600 + # This is assumed to be expected avg profit * expected trade count. # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# self.expected_max_profit = 3.85 +# expected max profit = 3.85 # Check that the reported Σ% values do not exceed this! # Note, this is ratio. 3.85 stated above means 385Σ%. EXPECTED_MAX_PROFIT = 3.0 -# max average trade duration in minutes -# if eval ends with higher value, we consider it a failed eval +# Max average trade duration in minutes. +# If eval ends with higher value, we consider it a failed eval. MAX_ACCEPTED_TRADE_DURATION = 300 diff --git a/freqtrade/optimize/hyperopt_loss_onlyprofit.py b/freqtrade/optimize/hyperopt_loss_onlyprofit.py new file mode 100644 index 000000000..4a1fabe35 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_onlyprofit.py @@ -0,0 +1,34 @@ +""" +OnlyProfitHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from pandas import DataFrame + +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# expected max profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + + +class OnlyProfitHyperOptLoss(IHyperOptLoss): + """ + Defines the loss function for hyperopt. + + This implementation takes only profit into account. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results. + """ + total_profit = results.profit_percent.sum() + return max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index be1a3d4b4..f74b27744 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -1,8 +1,9 @@ """ -IHyperOptLoss interface -This module defines the interface for the loss-function for hyperopts -""" +SharpeHyperOptLoss +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" from datetime import datetime from pandas import DataFrame @@ -13,8 +14,9 @@ from freqtrade.optimize.hyperopt import IHyperOptLoss class SharpeHyperOptLoss(IHyperOptLoss): """ - Defines the a loss function for hyperopt. - This implementation uses the sharpe ratio calculation. + Defines the loss function for hyperopt. + + This implementation uses the Sharpe Ratio calculation. """ @staticmethod @@ -22,8 +24,9 @@ class SharpeHyperOptLoss(IHyperOptLoss): min_date: datetime, max_date: datetime, *args, **kwargs) -> float: """ - Objective function, returns smaller number for more optimal results - Using sharpe ratio calculation + Objective function, returns smaller number for more optimal results. + + Uses Sharpe Ratio calculation. """ total_profit = results.profit_percent days_period = (max_date - min_date).days From cf6113068cdf1c9458a9491854df01e66306bf4b Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Tue, 23 Jul 2019 22:52:42 -0500 Subject: [PATCH 161/191] Resolve issue #2042 Issue #2042 noted that the terminal output from `setup.sh` regarding an option use the bot was missing from the documentation. This has been added. --- docs/bot-usage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index c643ecace..b29b046e5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -2,6 +2,8 @@ This page explains the different parameters of the bot and how to run it. +Note: You can now use the bot by executing `source .env/bin/activate; freqtrade`. + ## Bot commands From a0cecc6c522700e0884474d1f456fa03a3a22e01 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jul 2019 06:29:50 +0200 Subject: [PATCH 162/191] Fix test after pandas 0.25.0 update --- freqtrade/data/btanalysis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index dcd544d00..f2356c34b 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -67,7 +67,6 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int dates = pd.Series(pd.concat(dates).values, name='date') df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) - df2 = df2.astype(dtype={"open_time": "datetime64", "close_time": "datetime64"}) df2 = pd.concat([dates, df2], axis=1) df2 = df2.set_index('date') df_final = df2.resample(freq)[['pair']].count() From e9b77298a7ccb289da21c7eff311d3119619e1e2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Jul 2019 08:17:41 +0300 Subject: [PATCH 163/191] max() removed --- freqtrade/optimize/hyperopt_loss_onlyprofit.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_onlyprofit.py b/freqtrade/optimize/hyperopt_loss_onlyprofit.py index 4a1fabe35..a1c50e727 100644 --- a/freqtrade/optimize/hyperopt_loss_onlyprofit.py +++ b/freqtrade/optimize/hyperopt_loss_onlyprofit.py @@ -12,8 +12,12 @@ from freqtrade.optimize.hyperopt import IHyperOptLoss # This is assumed to be expected avg profit * expected trade count. # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, # expected max profit = 3.85 -# Check that the reported Σ% values do not exceed this! -# Note, this is ratio. 3.85 stated above means 385Σ%. +# +# Note, this is ratio. 3.85 stated above means 385Σ%, 3.0 means 300Σ%. +# +# In this implementation it's only used in calculation of the resulting value +# of the objective function as a normalization coefficient and does not +# represent any limit for profits as in the Freqtrade legacy default loss function. EXPECTED_MAX_PROFIT = 3.0 @@ -31,4 +35,4 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss): Objective function, returns smaller number for better results. """ total_profit = results.profit_percent.sum() - return max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + return 1 - total_profit / EXPECTED_MAX_PROFIT From 05be16e9e1dee7510bcf5ba1d5d9eadb53cca2a2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Jul 2019 08:49:33 +0300 Subject: [PATCH 164/191] helpstring alignment fixed --- docs/bot-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8031a2072..4462b63c6 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -201,7 +201,7 @@ to find optimal parameter values for your stategy. usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] [-r] - [--customhyperopt NAME] [--hyperopt-path PATH] + [--customhyperopt NAME] [--hyperopt-path PATH] [--eps] [-e INT] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--dmmp] [--print-all] [-j JOBS] From f58668fd67f971cb6b1b0074363112becb7da1f7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Jul 2019 20:54:12 +0300 Subject: [PATCH 165/191] test added --- freqtrade/tests/optimize/test_hyperopt.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a588bab64..fc71200a5 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -337,6 +337,24 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N assert under > correct +def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 hyperopt.log_results( From 05b185494650c1cab2a94f08b7fb7780de2b9db6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 19:56:59 +0200 Subject: [PATCH 166/191] Gracefully handle InvalidOrderException. --- freqtrade/freqtradebot.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b103d73a7..d52165e0a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -524,7 +524,11 @@ class FreqtradeBot(object): if trade.open_order_id: # Update trade with order values logger.info('Found open order for %s', trade) - order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) + try: + order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) + except InvalidOrderException as exception: + logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception) + return # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) @@ -749,7 +753,7 @@ class FreqtradeBot(object): if not trade.open_order_id: continue order = self.exchange.get_order(trade.open_order_id, trade.pair) - except (RequestException, DependencyException): + except (RequestException, DependencyException, InvalidOrderException): logger.info( 'Cannot query order for %s due to %s', trade, From e1b8ff798fb007ea75be49d69ce69cd2971d5d44 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:05:48 +0200 Subject: [PATCH 167/191] Add test to verify that get_order was successfully cought --- freqtrade/tests/test_freqtradebot.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 9a22a4f94..1a4c5159c 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1460,6 +1460,22 @@ def test_update_trade_state_exception(mocker, default_conf, assert log_has('Could not update trade amount: ', caplog.record_tuples) +def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.get_order', + MagicMock(side_effect=InvalidOrderException)) + + trade = MagicMock() + trade.open_order_id = '123' + trade.open_fee = 0.001 + + # Test raise of OperationalException exception + grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock()) + freqtrade.update_trade_state(trade) + assert grm_mock.call_count == 0 + assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog.record_tuples) + + def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # get_order should not be called!! From 4b8b2f7c5bcde38c234eca6f8d2b9f073ead3df8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:06:20 +0200 Subject: [PATCH 168/191] Use raise xxx from e to have a nicer traceback --- freqtrade/exchange/exchange.py | 62 +++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 65f013a03..a7c76e635 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -174,10 +174,10 @@ class Exchange(object): try: api = getattr(ccxt_module, name.lower())(ex_config) - except (KeyError, AttributeError): - raise OperationalException(f'Exchange {name} is not supported') + except (KeyError, AttributeError) as e: + raise OperationalException(f'Exchange {name} is not supported') from e except ccxt.BaseError as e: - raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") + raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e self.set_sandbox(api, exchange_config, name) @@ -398,17 +398,17 @@ class Exchange(object): raise DependencyException( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' - f'Message: {e}') + f'Message: {e}') from e except ccxt.InvalidOrder as e: raise DependencyException( f'Could not create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e def buy(self, pair: str, ordertype: str, amount: float, rate: float, time_in_force) -> Dict: @@ -493,9 +493,9 @@ class Exchange(object): return balances except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get balance due to {e.__class__.__name__}. Message: {e}') + f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_tickers(self) -> Dict: @@ -504,12 +504,12 @@ class Exchange(object): except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') + f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: @@ -528,9 +528,9 @@ class Exchange(object): return data except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') + f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e else: logger.info("returning cached ticker-data for %s", pair) return self._cached_ticker[pair] @@ -651,12 +651,12 @@ class Exchange(object): except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical candlestick data.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') + raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. ' + f'Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(f'Could not fetch ticker data. Msg: {e}') + raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e @retrier def cancel_order(self, order_id: str, pair: str) -> None: @@ -667,12 +667,12 @@ class Exchange(object): return self._api.cancel_order(order_id, pair) except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Could not cancel order. Message: {e}') + f'Could not cancel order. Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') + f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_order(self, order_id: str, pair: str) -> Dict: @@ -683,12 +683,12 @@ class Exchange(object): return self._api.fetch_order(order_id, pair) except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Tried to get an invalid order (id: {order_id}). Message: {e}') + f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get order due to {e.__class__.__name__}. Message: {e}') + f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_order_book(self, pair: str, limit: int = 100) -> dict: @@ -704,12 +704,12 @@ class Exchange(object): except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get order book due to {e.__class__.__name__}. Message: {e}') + f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: @@ -726,9 +726,9 @@ class Exchange(object): except ccxt.NetworkError as e: raise TemporaryError( - f'Could not get trades due to networking error. Message: {e}') + f'Could not get trades due to networking error. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, @@ -742,9 +742,9 @@ class Exchange(object): price=price, takerOrMaker=taker_or_maker)['rate'] except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') + f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e def is_exchange_bad(exchange: str) -> bool: From 10c69387fd7b86560082bcda2df92e48421a3304 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Jul 2019 21:06:41 +0300 Subject: [PATCH 169/191] docs adjusted --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ef3d28188..2755cae2d 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -156,10 +156,10 @@ Each hyperparameter tuning requires a target. This is usually defined as a loss By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. -A different version this can be used by using the `--hyperopt-loss ` argument. -This class should be in it's own file within the `user_data/hyperopts/` directory. +A different loss function can be specified by using the `--hyperopt-loss ` argument. +This class should be in its own file within the `user_data/hyperopts/` directory. -Currently, the following loss functions are builtin: `SharpeHyperOptLoss` and `DefaultHyperOptLoss`. +Currently, the following loss functions are builtin: `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function), `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns) and `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration). ### Creating and using a custom loss function From 7ee971c3e3a0b6b3b2bfb87806949e455fbacc17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:35:20 +0200 Subject: [PATCH 170/191] Add simple method to add deprecations to cmd line options --- freqtrade/configuration/configuration.py | 10 +++++--- freqtrade/tests/test_configuration.py | 29 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index c97bd277a..17ad37d6a 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -4,6 +4,7 @@ This module contains the configuration class import json import logging import sys +import warnings from argparse import Namespace from typing import Any, Callable, Dict, Optional @@ -15,7 +16,6 @@ from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode - logger = logging.getLogger(__name__) @@ -187,7 +187,8 @@ class Configuration(object): 'Using ticker_interval: {} ...') self._args_to_config(config, argname='live', - logstring='Parameter -l/--live detected ...') + logstring='Parameter -l/--live detected ...', + deprecated_msg='--live will be removed soon.') self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') @@ -323,7 +324,8 @@ class Configuration(object): 'to be greater than trailing_stop_positive_offset in your config.') def _args_to_config(self, config: Dict[str, Any], argname: str, - logstring: str, logfun: Optional[Callable] = None) -> None: + logstring: str, logfun: Optional[Callable] = None, + deprecated_msg: Optional[str] = None) -> None: """ :param config: Configuration dictionary :param argname: Argumentname in self.args - will be copied to config dict. @@ -340,3 +342,5 @@ class Configuration(object): logger.info(logstring.format(logfun(config[argname]))) else: logger.info(logstring.format(config[argname])) + if deprecated_msg: + warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index d3567e943..56ff79625 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -1,10 +1,11 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name import json import logging +import warnings from argparse import Namespace from copy import deepcopy -from unittest.mock import MagicMock from pathlib import Path +from unittest.mock import MagicMock import pytest from jsonschema import Draft4Validator, ValidationError, validate @@ -62,6 +63,32 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: assert validated_conf.items() >= default_conf.items() +def test__args_to_config(caplog): + + arg_list = ['--strategy-path', 'TestTest'] + args = Arguments(arg_list, '').get_parsed_arg() + configuration = Configuration(args) + config = {} + with warnings.catch_warnings(record=True) as w: + # No warnings ... + configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef") + assert len(w) == 0 + assert log_has("DeadBeef", caplog.record_tuples) + assert config['strategy_path'] == "TestTest" + + configuration = Configuration(args) + config = {} + with warnings.catch_warnings(record=True) as w: + # Deprecation warnings! + configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef", + deprecated_msg="Going away soon!") + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "DEPRECATED: Going away soon!" in str(w[-1].message) + assert log_has("DeadBeef", caplog.record_tuples) + assert config['strategy_path'] == "TestTest" + + def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = 0 patched_configuration_load_config_file(mocker, default_conf) From 0c14176cd7a32abf76cf3448e2ec93c9e5f9f33a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:36:19 +0200 Subject: [PATCH 171/191] Deprecate --live --- docs/deprecated.md | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/docs/deprecated.md b/docs/deprecated.md index c1582ce31..2bf655191 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,31 +4,16 @@ This page contains description of the command line arguments, configuration para and the bot features that were declared as DEPRECATED by the bot development team and are no longer supported. Please avoid their usage in your configuration. +### the `--live` command line option + +`--live` in the context of backtesting allows to download the latest tick data for backtesting. +Since this only downloads one set of data (by default 500 candles) - this is not really suitable for extendet backtesting, and has therefore been deprecated. + +This command was deprecated in `2019.6-dev` and will be removed after the next release. + +## Removed features + ### The **--dynamic-whitelist** command line option This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) and in freqtrade 2019.7 (master branch). - -Per default `--dynamic-whitelist` will retrieve the 20 currencies based -on BaseVolume. This value can be changed when you run the script. - -**By Default** -Get the 20 currencies based on BaseVolume. - -```bash -freqtrade --dynamic-whitelist -``` - -**Customize the number of currencies to retrieve** -Get the 30 currencies based on BaseVolume. - -```bash -freqtrade --dynamic-whitelist 30 -``` - -**Exception** -`--dynamic-whitelist` must be greater than 0. If you enter 0 or a -negative value (e.g -2), `--dynamic-whitelist` will use the default -value (20). - - From 3c3a902a69fbeecdf544a10b83babb8ce424230e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:42:08 +0200 Subject: [PATCH 172/191] Move argument definitions to their own file --- freqtrade/configuration/arguments.py | 300 +----------------------- freqtrade/configuration/cli_options.py | 302 +++++++++++++++++++++++++ 2 files changed, 304 insertions(+), 298 deletions(-) create mode 100644 freqtrade/configuration/cli_options.py diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index c70d1b4d2..988f485dc 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -2,308 +2,12 @@ This module contains the argument manager class """ import argparse -import os import re from typing import List, NamedTuple, Optional import arrow -from freqtrade import __version__, constants - - -def check_int_positive(value: str) -> int: - try: - uint = int(value) - if uint <= 0: - raise ValueError - except ValueError: - raise argparse.ArgumentTypeError( - f"{value} is invalid for this parameter, should be a positive integer value" - ) - return uint - - -class Arg: - # Optional CLI arguments - def __init__(self, *args, **kwargs): - self.cli = args - self.kwargs = kwargs - - -# List of available command line options -AVAILABLE_CLI_OPTIONS = { - # Common options - "verbosity": Arg( - '-v', '--verbose', - help='Verbose mode (-vv for more, -vvv to get all messages).', - action='count', - default=0, - ), - "logfile": Arg( - '--logfile', - help='Log to the file specified.', - metavar='FILE', - ), - "version": Arg( - '-V', '--version', - action='version', - version=f'%(prog)s {__version__}', - ), - "config": Arg( - '-c', '--config', - 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.', - action='append', - metavar='PATH', - ), - "datadir": Arg( - '-d', '--datadir', - help='Path to backtest data.', - metavar='PATH', - ), - # Main options - "strategy": Arg( - '-s', '--strategy', - help='Specify strategy class name (default: `%(default)s`).', - metavar='NAME', - default='DefaultStrategy', - ), - "strategy_path": Arg( - '--strategy-path', - help='Specify additional strategy lookup path.', - metavar='PATH', - ), - "db_url": Arg( - '--db-url', - help=f'Override trades database URL, this is useful in custom deployments ' - f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' - f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', - metavar='PATH', - ), - "sd_notify": Arg( - '--sd-notify', - help='Notify systemd service manager.', - action='store_true', - ), - # Optimize common - "ticker_interval": Arg( - '-i', '--ticker-interval', - help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', - ), - "timerange": Arg( - '--timerange', - help='Specify what timerange of data to use.', - ), - "max_open_trades": Arg( - '--max_open_trades', - help='Specify max_open_trades to use.', - type=int, - metavar='INT', - ), - "stake_amount": Arg( - '--stake_amount', - help='Specify stake_amount.', - type=float, - ), - "refresh_pairs": Arg( - '-r', '--refresh-pairs-cached', - help='Refresh the pairs files in tests/testdata with the latest data from the ' - 'exchange. Use it if you want to run your optimization commands with ' - 'up-to-date data.', - action='store_true', - ), - # Backtesting - "position_stacking": Arg( - '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking).', - action='store_true', - default=False, - ), - "use_max_market_positions": Arg( - '--dmmp', '--disable-max-market-positions', - help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number).', - action='store_false', - default=True, - ), - "live": Arg( - '-l', '--live', - help='Use live data.', - action='store_true', - ), - "strategy_list": Arg( - '--strategy-list', - help='Provide a comma-separated list of strategies to backtest. ' - 'Please note that ticker-interval needs to be set either in config ' - 'or via command line. When using this together with `--export trades`, ' - 'the strategy-name is injected into the filename ' - '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', - nargs='+', - ), - "export": Arg( - '--export', - help='Export backtest results, argument are: trades. ' - 'Example: `--export=trades`', - ), - "exportfilename": Arg( - '--export-filename', - help='Save backtest results to the file with this filename (default: `%(default)s`). ' - 'Requires `--export` to be set as well. ' - 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', - metavar='PATH', - default=os.path.join('user_data', 'backtest_data', - 'backtest-result.json'), - ), - # Edge - "stoploss_range": Arg( - '--stoplosses', - help='Defines a range of stoploss values against which edge will assess the strategy. ' - 'The format is "min,max,step" (without any space). ' - 'Example: `--stoplosses=-0.01,-0.1,-0.001`', - ), - # Hyperopt - "hyperopt": Arg( - '--customhyperopt', - help='Specify hyperopt class name (default: `%(default)s`).', - metavar='NAME', - default=constants.DEFAULT_HYPEROPT, - ), - "hyperopt_path": Arg( - '--hyperopt-path', - help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', - metavar='PATH', - ), - "epochs": Arg( - '-e', '--epochs', - help='Specify number of epochs (default: %(default)d).', - type=check_int_positive, - metavar='INT', - default=constants.HYPEROPT_EPOCH, - ), - "spaces": Arg( - '-s', '--spaces', - help='Specify which parameters to hyperopt. Space-separated list. ' - 'Default: `%(default)s`.', - choices=['all', 'buy', 'sell', 'roi', 'stoploss'], - nargs='+', - default='all', - ), - "print_all": Arg( - '--print-all', - help='Print all results, not only the best ones.', - action='store_true', - default=False, - ), - "hyperopt_jobs": Arg( - '-j', '--job-workers', - help='The number of concurrently running jobs for hyperoptimization ' - '(hyperopt worker processes). ' - 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' - 'If 1 is given, no parallel computing code is used at all.', - type=int, - metavar='JOBS', - default=-1, - ), - "hyperopt_random_state": Arg( - '--random-state', - help='Set random state to some positive integer for reproducible hyperopt results.', - type=check_int_positive, - metavar='INT', - ), - "hyperopt_min_trades": Arg( - '--min-trades', - help="Set minimal desired number of trades for evaluations in the hyperopt " - "optimization path (default: 1).", - type=check_int_positive, - metavar='INT', - default=1, - ), - "hyperopt_continue": Arg( - "--continue", - help="Continue hyperopt from previous runs. " - "By default, temporary files will be removed and hyperopt will start from scratch.", - default=False, - action='store_true', - ), - "hyperopt_loss": Arg( - '--hyperopt-loss', - help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' - 'Different functions can generate completely different results, ' - 'since the target for optimization is different. (default: `%(default)s`).', - metavar='NAME', - default=constants.DEFAULT_HYPEROPT_LOSS, - ), - # List exchanges - "print_one_column": Arg( - '-1', '--one-column', - help='Print exchanges in one column.', - action='store_true', - ), - # Script options - "pairs": Arg( - '-p', '--pairs', - help='Show profits for only these pairs. Pairs are comma-separated.', - ), - # Download data - "pairs_file": Arg( - '--pairs-file', - help='File containing a list of pairs to download.', - metavar='FILE', - ), - "days": Arg( - '--days', - help='Download data for given number of days.', - type=check_int_positive, - metavar='INT', - ), - "exchange": Arg( - '--exchange', - help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' - f'Only valid if no config is provided.', - ), - "timeframes": Arg( - '-t', '--timeframes', - 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='+', - ), - "erase": Arg( - '--erase', - help='Clean all existing data for the selected exchange/pairs/timeframes.', - action='store_true', - ), - # Plot dataframe - "indicators1": Arg( - '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. ' - 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', - default='sma,ema3,ema5', - ), - "indicators2": Arg( - '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. ' - 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', - default='macd,macdsignal', - ), - "plot_limit": Arg( - '--plot-limit', - help='Specify tick limit for plotting. Notice: too high values cause huge files. ' - 'Default: %(default)s.', - type=check_int_positive, - metavar='INT', - default=750, - ), - "trade_source": Arg( - '--trade-source', - help='Specify the source for trades (Can be DB or file (backtest file)) ' - 'Default: %(default)s', - choices=["DB", "file"], - default="file", - ), -} - +from freqtrade.arguments.cli_options import AVAILABLE_CLI_OPTIONS +from freqtrade import constants ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py new file mode 100644 index 000000000..04fde2051 --- /dev/null +++ b/freqtrade/configuration/cli_options.py @@ -0,0 +1,302 @@ +""" +Definition of cli arguments used in arguments.py +""" +import argparse +import os + +from freqtrade import __version__, constants + + +def check_int_positive(value: str) -> int: + try: + uint = int(value) + if uint <= 0: + raise ValueError + except ValueError: + raise argparse.ArgumentTypeError( + f"{value} is invalid for this parameter, should be a positive integer value" + ) + return uint + + +class Arg: + # Optional CLI arguments + def __init__(self, *args, **kwargs): + self.cli = args + self.kwargs = kwargs + + +# List of available command line options +AVAILABLE_CLI_OPTIONS = { + # Common options + "verbosity": Arg( + '-v', '--verbose', + help='Verbose mode (-vv for more, -vvv to get all messages).', + action='count', + default=0, + ), + "logfile": Arg( + '--logfile', + help='Log to the file specified.', + metavar='FILE', + ), + "version": Arg( + '-V', '--version', + action='version', + version=f'%(prog)s {__version__}', + ), + "config": Arg( + '-c', '--config', + 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.', + action='append', + metavar='PATH', + ), + "datadir": Arg( + '-d', '--datadir', + help='Path to backtest data.', + metavar='PATH', + ), + # Main options + "strategy": Arg( + '-s', '--strategy', + help='Specify strategy class name (default: `%(default)s`).', + metavar='NAME', + default='DefaultStrategy', + ), + "strategy_path": Arg( + '--strategy-path', + help='Specify additional strategy lookup path.', + metavar='PATH', + ), + "db_url": Arg( + '--db-url', + help=f'Override trades database URL, this is useful in custom deployments ' + f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' + f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', + metavar='PATH', + ), + "sd_notify": Arg( + '--sd-notify', + help='Notify systemd service manager.', + action='store_true', + ), + # Optimize common + "ticker_interval": Arg( + '-i', '--ticker-interval', + help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', + ), + "timerange": Arg( + '--timerange', + help='Specify what timerange of data to use.', + ), + "max_open_trades": Arg( + '--max_open_trades', + help='Specify max_open_trades to use.', + type=int, + metavar='INT', + ), + "stake_amount": Arg( + '--stake_amount', + help='Specify stake_amount.', + type=float, + ), + "refresh_pairs": Arg( + '-r', '--refresh-pairs-cached', + help='Refresh the pairs files in tests/testdata with the latest data from the ' + 'exchange. Use it if you want to run your optimization commands with ' + 'up-to-date data.', + action='store_true', + ), + # Backtesting + "position_stacking": Arg( + '--eps', '--enable-position-stacking', + help='Allow buying the same pair multiple times (position stacking).', + action='store_true', + default=False, + ), + "use_max_market_positions": Arg( + '--dmmp', '--disable-max-market-positions', + help='Disable applying `max_open_trades` during backtest ' + '(same as setting `max_open_trades` to a very high number).', + action='store_false', + default=True, + ), + "live": Arg( + '-l', '--live', + help='Use live data.', + action='store_true', + ), + "strategy_list": Arg( + '--strategy-list', + help='Provide a comma-separated list of strategies to backtest. ' + 'Please note that ticker-interval needs to be set either in config ' + 'or via command line. When using this together with `--export trades`, ' + 'the strategy-name is injected into the filename ' + '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', + nargs='+', + ), + "export": Arg( + '--export', + help='Export backtest results, argument are: trades. ' + 'Example: `--export=trades`', + ), + "exportfilename": Arg( + '--export-filename', + help='Save backtest results to the file with this filename (default: `%(default)s`). ' + 'Requires `--export` to be set as well. ' + 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', + metavar='PATH', + default=os.path.join('user_data', 'backtest_data', + 'backtest-result.json'), + ), + # Edge + "stoploss_range": Arg( + '--stoplosses', + help='Defines a range of stoploss values against which edge will assess the strategy. ' + 'The format is "min,max,step" (without any space). ' + 'Example: `--stoplosses=-0.01,-0.1,-0.001`', + ), + # Hyperopt + "hyperopt": Arg( + '--customhyperopt', + help='Specify hyperopt class name (default: `%(default)s`).', + metavar='NAME', + default=constants.DEFAULT_HYPEROPT, + ), + "hyperopt_path": Arg( + '--hyperopt-path', + help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', + metavar='PATH', + ), + "epochs": Arg( + '-e', '--epochs', + help='Specify number of epochs (default: %(default)d).', + type=check_int_positive, + metavar='INT', + default=constants.HYPEROPT_EPOCH, + ), + "spaces": Arg( + '-s', '--spaces', + help='Specify which parameters to hyperopt. Space-separated list. ' + 'Default: `%(default)s`.', + choices=['all', 'buy', 'sell', 'roi', 'stoploss'], + nargs='+', + default='all', + ), + "print_all": Arg( + '--print-all', + help='Print all results, not only the best ones.', + action='store_true', + default=False, + ), + "hyperopt_jobs": Arg( + '-j', '--job-workers', + help='The number of concurrently running jobs for hyperoptimization ' + '(hyperopt worker processes). ' + 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' + 'If 1 is given, no parallel computing code is used at all.', + type=int, + metavar='JOBS', + default=-1, + ), + "hyperopt_random_state": Arg( + '--random-state', + help='Set random state to some positive integer for reproducible hyperopt results.', + type=check_int_positive, + metavar='INT', + ), + "hyperopt_min_trades": Arg( + '--min-trades', + help="Set minimal desired number of trades for evaluations in the hyperopt " + "optimization path (default: 1).", + type=check_int_positive, + metavar='INT', + default=1, + ), + "hyperopt_continue": Arg( + "--continue", + help="Continue hyperopt from previous runs. " + "By default, temporary files will be removed and hyperopt will start from scratch.", + default=False, + action='store_true', + ), + "hyperopt_loss": Arg( + '--hyperopt-loss', + help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' + 'Different functions can generate completely different results, ' + 'since the target for optimization is different. (default: `%(default)s`).', + metavar='NAME', + default=constants.DEFAULT_HYPEROPT_LOSS, + ), + # List exchanges + "print_one_column": Arg( + '-1', '--one-column', + help='Print exchanges in one column.', + action='store_true', + ), + # Script options + "pairs": Arg( + '-p', '--pairs', + help='Show profits for only these pairs. Pairs are comma-separated.', + ), + # Download data + "pairs_file": Arg( + '--pairs-file', + help='File containing a list of pairs to download.', + metavar='FILE', + ), + "days": Arg( + '--days', + help='Download data for given number of days.', + type=check_int_positive, + metavar='INT', + ), + "exchange": Arg( + '--exchange', + help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' + f'Only valid if no config is provided.', + ), + "timeframes": Arg( + '-t', '--timeframes', + 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='+', + ), + "erase": Arg( + '--erase', + help='Clean all existing data for the selected exchange/pairs/timeframes.', + action='store_true', + ), + # Plot dataframe + "indicators1": Arg( + '--indicators1', + help='Set indicators from your strategy you want in the first row of the graph. ' + 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', + default='sma,ema3,ema5', + ), + "indicators2": Arg( + '--indicators2', + help='Set indicators from your strategy you want in the third row of the graph. ' + 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', + default='macd,macdsignal', + ), + "plot_limit": Arg( + '--plot-limit', + help='Specify tick limit for plotting. Notice: too high values cause huge files. ' + 'Default: %(default)s.', + type=check_int_positive, + metavar='INT', + default=750, + ), + "trade_source": Arg( + '--trade-source', + help='Specify the source for trades (Can be DB or file (backtest file)) ' + 'Default: %(default)s', + choices=["DB", "file"], + default="file", + ), +} From bf1c197a37ac73a4ba96797f8c3d5f7b01c648a0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 02:21:31 +0300 Subject: [PATCH 173/191] import errors fixed --- freqtrade/configuration/arguments.py | 2 +- freqtrade/tests/test_arguments.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 988f485dc..4f0c3d31b 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -6,7 +6,7 @@ import re from typing import List, NamedTuple, Optional import arrow -from freqtrade.arguments.cli_options import AVAILABLE_CLI_OPTIONS +from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade import constants ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 3de71a621..bf744f72b 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -5,7 +5,7 @@ import pytest from freqtrade.configuration import Arguments, TimeRange from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME -from freqtrade.configuration.arguments import check_int_positive +from freqtrade.configuration.cli_options import check_int_positive # Parse common command-line-arguments. Used for all tools From 327e505273830f80280867bf6a9d572310b3e7b4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 02:57:51 +0300 Subject: [PATCH 174/191] non-working link to misc.py removed --- docs/bot-usage.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4462b63c6..9af3692f8 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -294,11 +294,6 @@ optional arguments: To understand edge and how to read the results, please read the [edge documentation](edge.md). -## A parameter missing in the configuration? - -All parameters for `main.py`, `backtesting`, `hyperopt` are referenced -in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84) - ## Next step The optimal strategy of the bot will change with time depending of the market trends. The next step is to From 1ac4a7e11609e0ece9640dd60013730169751310 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 02:59:10 +0300 Subject: [PATCH 175/191] rendering for a Note fixed --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 657273e2f..74b3a3202 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -215,7 +215,7 @@ The `freqtrade.service.watchdog` file contains an example of the service unit co as the watchdog. !!! Note - The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. + The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. ------ From 785a7a22bca50e645b831caf67815db1fbc09b1e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 04:02:34 +0300 Subject: [PATCH 176/191] output divider in logs between throttles --- freqtrade/worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index c224b4ee5..db0dba0e8 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -128,6 +128,7 @@ class Worker(object): return result def _process(self) -> bool: + logger.debug("========================================") state_changed = False try: state_changed = self.freqtrade.process() From c2deb1db255e5a091daa38460212b8a593141ef4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 02:05:20 +0300 Subject: [PATCH 177/191] eliminate warnings in pytest when testing handling of the deprecated strategy interfaces --- freqtrade/tests/strategy/test_strategy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 609cc58ff..df8c0f126 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -366,6 +366,7 @@ def test_strategy_override_use_sell_profit_only(caplog): ) in caplog.record_tuples +@pytest.mark.filterwarnings("ignore:deprecated") def test_deprecate_populate_indicators(result): default_location = path.join(path.dirname(path.realpath(__file__))) resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', @@ -398,6 +399,7 @@ def test_deprecate_populate_indicators(result): in str(w[-1].message) +@pytest.mark.filterwarnings("ignore:deprecated") def test_call_deprecated_function(result, monkeypatch): default_location = path.join(path.dirname(path.realpath(__file__))) resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', From bc299067aa2d8f42e9dbc2bb178083f19c09410d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 27 Jul 2019 23:24:06 +0300 Subject: [PATCH 178/191] get rid of pandas warning in pytest --- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index fc71200a5..fad89e877 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -297,7 +297,7 @@ def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_resu def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None: resultsb = hyperopt_results.copy() - resultsb['trade_duration'][1] = 20 + resultsb.loc[1, 'trade_duration'] = 20 hl = HyperOptLossResolver(default_conf).hyperoptloss longer = hl.hyperopt_loss_function(hyperopt_results, 100) From 08a3d2632859c07ceaf9be05dc668799be5dd73e Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 27 Jul 2019 18:35:21 -0500 Subject: [PATCH 179/191] Update bot-usage.md Update in response to feedback. --- docs/bot-usage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index b29b046e5..bc0d0a87c 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -2,7 +2,8 @@ This page explains the different parameters of the bot and how to run it. -Note: You can now use the bot by executing `source .env/bin/activate; freqtrade`. +!Note: + If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. ## Bot commands From b691fb7f2dc04b9def11a56e6746381622d144aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jul 2019 15:19:17 +0200 Subject: [PATCH 180/191] Fix some hyperopt tests --- freqtrade/tests/optimize/test_hyperopt.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index fad89e877..065b4c41b 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -8,7 +8,7 @@ import pytest from arrow import Arrow from filelock import Timeout -from freqtrade import DependencyException +from freqtrade import DependencyException, OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize import setup_configuration, start_hyperopt @@ -186,6 +186,13 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: assert hasattr(x, "ticker_interval") +def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None: + default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): + HyperOptResolver(default_conf, ).hyperopt + + def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: hl = DefaultHyperOptLoss @@ -193,9 +200,15 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', MagicMock(return_value=hl) ) - x = HyperOptResolver(default_conf, ).hyperopt - assert hasattr(x, "populate_indicators") - assert hasattr(x, "ticker_interval") + x = HyperOptLossResolver(default_conf, ).hyperoptloss + assert hasattr(x, "hyperopt_loss_function") + + +def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None: + default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): + HyperOptLossResolver(default_conf, ).hyperopt def test_start(mocker, default_conf, caplog) -> None: From 02bfe2dad3d9ad8c3f1aeba2023c703a20f2e9e2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:06 +0000 Subject: [PATCH 181/191] Update numpy from 1.16.4 to 1.17.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 52442fb19..be1495d08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Load common requirements -r requirements-common.txt -numpy==1.16.4 +numpy==1.17.0 pandas==0.24.2 scipy==1.3.0 From 5a6e20a6aaa3174623db908a6ab8007143f554a5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:07 +0000 Subject: [PATCH 182/191] Update pandas from 0.24.2 to 0.25.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index be1495d08..6420c7879 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ -r requirements-common.txt numpy==1.17.0 -pandas==0.24.2 +pandas==0.25.0 scipy==1.3.0 From fe088dc8c32bfe2f1378b5a361e198bcd8fbd042 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:08 +0000 Subject: [PATCH 183/191] Update ccxt from 1.18.860 to 1.18.992 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 409c979b5..0d6c3f217 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.860 +ccxt==1.18.992 SQLAlchemy==1.3.5 python-telegram-bot==11.1.0 arrow==0.14.2 From 0fd91e4450e97acb05635aac63e62fe6311dc926 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:09 +0000 Subject: [PATCH 184/191] Update sqlalchemy from 1.3.5 to 1.3.6 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 0d6c3f217..a1cdbc6c0 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.18.992 -SQLAlchemy==1.3.5 +SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.2 cachetools==3.1.1 From 9f70ebecf16fd7afaa51ddfec57796d706cc7e82 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:10 +0000 Subject: [PATCH 185/191] Update arrow from 0.14.2 to 0.14.3 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index a1cdbc6c0..b79c700c8 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.992 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 -arrow==0.14.2 +arrow==0.14.3 cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore From a3620c60adbbed1de171bbbaf03174cf973d7cc4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:11 +0000 Subject: [PATCH 186/191] Update flask from 1.0.3 to 1.1.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b79c700c8..2e52b84ad 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -29,4 +29,4 @@ python-rapidjson==0.7.2 sdnotify==0.3.2 # Api server -flask==1.0.3 +flask==1.1.1 From ebca1e435745c2c7c72805c25d6629fe0cc0df92 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:12 +0000 Subject: [PATCH 187/191] Update flake8 from 3.7.7 to 3.7.8 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 83fec3b8a..28fd19ce4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ -r requirements.txt -r requirements-plot.txt -flake8==3.7.7 +flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 pytest==5.0.0 From 0f632201e07c06f2f95cd48661857b79a00b488f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:14 +0000 Subject: [PATCH 188/191] Update pytest from 5.0.0 to 5.0.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 28fd19ce4..e6f7bde1c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==5.0.0 +pytest==5.0.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From 3e95b7d8a51c2a12d074223eecc9ab90ecd2f913 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:15 +0000 Subject: [PATCH 189/191] Update mypy from 0.711 to 0.720 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e6f7bde1c..946d63039 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,4 +11,4 @@ pytest-asyncio==0.10.0 pytest-cov==2.7.1 pytest-random-order==1.0.4 coveralls==1.8.1 -mypy==0.711 +mypy==0.720 From 5ba0aa8082b1c801f7cd99dca8356e32f25ff23d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:16 +0000 Subject: [PATCH 190/191] Update plotly from 3.10.0 to 4.0.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index d4e4fc165..a6753fc3f 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.10.0 +plotly==4.0.0 From e64509f1b42d95c4a86a6b6663dbf15c27bae6ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:27:50 +0200 Subject: [PATCH 191/191] Version bump to 2019.7 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2fdcccea5..10900bee9 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.6-dev' +__version__ = '2019.7' class DependencyException(Exception):