From 107f00ff8f23f2721da9aa41d2a3fa9d71538832 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 20:17:15 +0200 Subject: [PATCH 01/24] 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 02/24] 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 03/24] 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 2a20423be6b10cf1e1c6d38c29ab48a919557007 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 21:35:42 +0200 Subject: [PATCH 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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 10/24] 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 11/24] 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 12/24] 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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 8ccfc0f31635da57045fcf02b012d17178d7de06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 06:24:40 +0200 Subject: [PATCH 17/24] 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 18/24] 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 19/24] 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 20/24] 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 21/24] 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 22/24] 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 e01c0ab4d680d72a4141ce5a5e60462027a67f31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jul 2019 20:02:28 +0200 Subject: [PATCH 23/24] 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 fa8904978b9b4bd27a4daba95d7136e7b0a31712 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jul 2019 06:31:49 +0200 Subject: [PATCH 24/24] 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`).',