Allow loading custom hyperopt loss functions

This commit is contained in:
Matthias 2019-07-15 21:35:42 +02:00
parent 2fedae6060
commit 2a20423be6
6 changed files with 135 additions and 24 deletions

View File

@ -230,6 +230,14 @@ AVAILABLE_CLI_OPTIONS = {
default=False, default=False,
action='store_true', 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 # List exchanges
"print_one_column": Arg( "print_one_column": Arg(
'-1', '--one-column', '-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", ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces",
"use_max_market_positions", "print_all", "hyperopt_jobs", "use_max_market_positions", "print_all", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades", "hyperopt_random_state", "hyperopt_min_trades",
"hyperopt_clean_state"] "hyperopt_clean_state", "loss_function"]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]

View File

@ -284,9 +284,13 @@ class Configuration(object):
self._args_to_config(config, argname='hyperopt_min_trades', self._args_to_config(config, argname='hyperopt_min_trades',
logstring='Parameter --min-trades detected: {}') logstring='Parameter --min-trades detected: {}')
self._args_to_config(config, argname='hyperopt_clean_state', self._args_to_config(config, argname='hyperopt_clean_state',
logstring='Removing hyperopt temp files') logstring='Removing hyperopt temp files')
self._args_to_config(config, argname='loss_function',
logstring='Using loss function: {}')
return config return config
def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]: def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]:

View File

@ -1,16 +1,30 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # 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 import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
from typing import Dict, Any, Callable, List
from functools import reduce
from skopt.space import Categorical, Dimension, Integer, Real from skopt.space import Categorical, Dimension, Integer, Real
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt 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): class DefaultHyperOpts(IHyperOpt):
@ -19,6 +33,21 @@ class DefaultHyperOpts(IHyperOpt):
You can override it with your own hyperopt 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 @staticmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)

View File

@ -7,7 +7,7 @@ This module contains the hyperopt logic
import logging import logging
import os import os
import sys import sys
from math import exp
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
from pprint import pprint from pprint import pprint
@ -22,6 +22,7 @@ from freqtrade.configuration import Arguments
from freqtrade.data.history import load_data, get_timeframe from freqtrade.data.history import load_data, get_timeframe
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -69,6 +70,20 @@ class Hyperopt(Backtesting):
self.trials_file = TRIALSDATA_PICKLE self.trials_file = TRIALSDATA_PICKLE
self.trials: List = [] 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) # Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_buy_trend'): if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
@ -160,16 +175,6 @@ class Hyperopt(Backtesting):
print('.', end='') print('.', end='')
sys.stdout.flush() 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: def has_space(self, space: str) -> bool:
""" """
Tell if a space value is contained in the configuration Tell if a space value is contained in the configuration
@ -231,9 +236,7 @@ class Hyperopt(Backtesting):
) )
result_explanation = self.format_results(results) result_explanation = self.format_results(results)
total_profit = results.profit_percent.sum()
trade_count = len(results.index) trade_count = len(results.index)
trade_duration = results.trade_duration.mean()
# If this evaluation contains too short amount of trades to be # If this evaluation contains too short amount of trades to be
# interesting -- consider it as 'bad' (assigned max. loss value) # interesting -- consider it as 'bad' (assigned max. loss value)
@ -246,7 +249,8 @@ class Hyperopt(Backtesting):
'result': result_explanation, '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 { return {
'loss': loss, 'loss': loss,

View File

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

View File

@ -1,17 +1,31 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # 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 import talib.abstract as ta
from pandas import DataFrame 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 from skopt.space import Categorical, Dimension, Integer, Real
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt 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. # This class is a sample. Feel free to customize it.
@ -28,6 +42,21 @@ class SampleHyperOpts(IHyperOpt):
roi_space, generate_roi_table, stoploss_space 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 @staticmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)