Update hyperopt-loss to use resolver

This commit is contained in:
Matthias 2019-07-16 06:27:23 +02:00
parent 7d62bb8c53
commit d23179e25c
9 changed files with 177 additions and 120 deletions

View File

@ -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 <value>` 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 <Class-name>` 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()

View File

@ -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"]

View File

@ -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'

View File

@ -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):
"""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
"""

View File

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