From 2a20423be6b10cf1e1c6d38c29ab48a919557007 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 21:35:42 +0200 Subject: [PATCH] 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)