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." + )