Merge pull request #2024 from freqtrade/custom_hyperopt_loss

Custom hyperopt loss function (and sharpe-ratio)
This commit is contained in:
Matthias 2019-07-20 12:48:26 +02:00 committed by GitHub
commit 790838d897
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 541 additions and 101 deletions

View File

@ -199,19 +199,22 @@ to find optimal parameter values for your stategy.
``` ```
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades MAX_OPEN_TRADES] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [-r] [--stake_amount STAKE_AMOUNT] [-r]
[--customhyperopt NAME] [--eps] [--dmmp] [-e INT] [--customhyperopt NAME] [--eps] [-e INT]
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
[--print-all] [-j JOBS] [--dmmp] [--print-all] [-j JOBS]
[--random-state INT] [--min-trades INT] [--continue]
[--hyperopt-loss NAME]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL -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 --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--max_open_trades MAX_OPEN_TRADES --max_open_trades INT
Specify max_open_trades to use. Specify max_open_trades to use.
--stake_amount STAKE_AMOUNT --stake_amount STAKE_AMOUNT
Specify stake_amount. Specify stake_amount.
@ -221,18 +224,18 @@ optional arguments:
run your optimization commands with up-to-date data. run your optimization commands with up-to-date data.
--customhyperopt NAME --customhyperopt NAME
Specify hyperopt class name (default: Specify hyperopt class name (default:
DefaultHyperOpts). `DefaultHyperOpts`).
--eps, --enable-position-stacking --eps, --enable-position-stacking
Allow buying the same pair multiple times (position Allow buying the same pair multiple times (position
stacking). 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 --dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high (same as setting `max_open_trades` to a very high
number). 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. --print-all Print all results, not only the best ones.
-j JOBS, --job-workers JOBS -j JOBS, --job-workers JOBS
The number of concurrently running jobs for The number of concurrently running jobs for
@ -240,6 +243,19 @@ optional arguments:
(default), all CPUs are used, for -2, all CPUs but one (default), all CPUs are used, for -2, all CPUs but one
are used, etc. If 1 is given, no parallel computing are used, etc. If 1 is given, no parallel computing
code is used at all. 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 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 ## Edge commands

View File

@ -144,16 +144,85 @@ 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 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 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. 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 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`. 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 (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
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-name>` 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`.
### 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 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)
``` python
from freqtrade.optimize.hyperopt import IHyperOptLoss
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_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*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
* 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()
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
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
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.
!!! Note
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later.
## Execute Hyperopt ## Execute Hyperopt
Once you have updated your hyperopt configuration you can run it. 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. We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
@ -168,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. 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 !!! 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 ### Execute Hyperopt with Different Ticker-Data Source
@ -179,12 +251,11 @@ use data from directory `user_data/data`.
### Running Hyperopt with Smaller Testset ### Running Hyperopt with Smaller Testset
Use the `--timerange` argument to change how much of the testset Use the `--timerange` argument to change how much of the testset you want to use.
you want to use. The last N ticks/timeframes will be used. For example, to use one month of data, pass the following parameter to the hyperopt call:
Example:
```bash ```bash
freqtrade hyperopt --timerange -200 freqtrade hyperopt --timerange 20180401-20180501
``` ```
### Running Hyperopt with Smaller Search Space ### Running Hyperopt with Smaller Search Space
@ -197,14 +268,14 @@ new buy strategy you have.
Legal values are: Legal values are:
- `all`: optimize everything * `all`: optimize everything
- `buy`: just search for a new buy strategy * `buy`: just search for a new buy strategy
- `sell`: just search for a new sell strategy * `sell`: just search for a new sell strategy
- `roi`: just optimize the minimal profit table for your strategy * `roi`: just optimize the minimal profit table for your strategy
- `stoploss`: search for the best stoploss value * `stoploss`: search for the best stoploss value
- space-separated list of any of the above values for example `--spaces roi stoploss` * 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 In some situations, you may need to run Hyperopt (and Backtesting) with the
`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. `--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments.
@ -252,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: 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) (dataframe['rsi'] < 29.0)
``` ```

View File

@ -213,6 +213,21 @@ AVAILABLE_CLI_OPTIONS = {
metavar='INT', metavar='INT',
default=1, default=1,
), ),
"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',
),
"hyperopt_loss": Arg(
'--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`).',
metavar='NAME',
default=constants.DEFAULT_HYPEROPT_LOSS,
),
# List exchanges # List exchanges
"print_one_column": Arg( "print_one_column": Arg(
'-1', '--one-column', '-1', '--one-column',
@ -299,7 +314,8 @@ 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_continue", "hyperopt_loss"]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]

View File

@ -259,6 +259,12 @@ 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_continue',
logstring='Hyperopt continue: {}')
self._args_to_config(config, argname='hyperopt_loss',
logstring='Using loss function: {}')
def _process_plot_options(self, config: Dict[str, Any]) -> None: def _process_plot_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='pairs', self._args_to_config(config, argname='pairs',

View File

@ -12,6 +12,7 @@ HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_HYPEROPT = 'DefaultHyperOpts' DEFAULT_HYPEROPT = 'DefaultHyperOpts'
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://' DEFAULT_DB_DRYRUN_URL = 'sqlite://'
UNLIMITED_STAKE_AMOUNT = 'unlimited' UNLIMITED_STAKE_AMOUNT = 'unlimited'

View File

@ -1,17 +1,15 @@
# 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 typing import Any, Callable, Dict, List
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'
class DefaultHyperOpts(IHyperOpt): class DefaultHyperOpts(IHyperOpt):
""" """

View File

@ -0,0 +1,53 @@
"""
DefaultHyperOptLoss
This module defines the default HyperoptLoss class which is being used for
Hyperoptimization.
"""
from math import exp
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 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 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()
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

@ -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
@ -21,7 +21,9 @@ from skopt.space import Dimension
from freqtrade.configuration import Arguments 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 # 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
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,27 +48,46 @@ class Hyperopt(Backtesting):
super().__init__(config) super().__init__(config)
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
# set TARGET_TRADES to suit your number concurrent trades so its realistic self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
# to the number of days self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
self.target_trades = 600
self.total_tries = config.get('epochs', 0) self.total_tries = config.get('epochs', 0)
self.current_best_loss = 100 self.current_best_loss = 100
# max average trade duration in minutes if not self.config.get('hyperopt_continue'):
# if eval ends with higher value, we consider it a failed eval self.clean_hyperopt()
self.max_accepted_trade_duration = 300 else:
logger.info("Continuing on previous hyperopt results.")
# 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
# Previous evaluations # Previous evaluations
self.trials_file = TRIALSDATA_PICKLE self.trials_file = TRIALSDATA_PICKLE
self.trials: List = [] 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
self.position_stacking = self.config.get('position_stacking', False),
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): def get_args(self, params):
dimensions = self.hyperopt_space() dimensions = self.hyperopt_space()
# Ensure the number of dimensions match # Ensure the number of dimensions match
@ -134,16 +155,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
@ -172,49 +183,40 @@ class Hyperopt(Backtesting):
return spaces return spaces
def generate_optimizer(self, _params: Dict) -> Dict: 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) params = self.get_args(_params)
if self.has_space('roi'): if self.has_space('roi'):
self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params)
if self.has_space('buy'): if self.has_space('buy'):
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) 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'): if self.has_space('sell'):
self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) 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'): if self.has_space('stoploss'):
self.strategy.stoploss = params['stoploss'] self.strategy.stoploss = params['stoploss']
processed = load(TICKERDATA_PICKLE) 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) min_date, max_date = get_timeframe(processed)
results = self.backtest( results = self.backtest(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': processed, 'processed': processed,
'max_open_trades': max_open_trades, 'max_open_trades': self.max_open_trades,
'position_stacking': self.config.get('position_stacking', False), 'position_stacking': self.position_stacking,
'start_date': min_date, 'start_date': min_date,
'end_date': max_date, 'end_date': max_date,
} }
) )
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)
@ -227,7 +229,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,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

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

View File

@ -8,8 +8,9 @@ from pathlib import Path
from typing import Optional, Dict from typing import Optional, Dict
from freqtrade import OperationalException 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_interface import IHyperOpt
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -54,7 +55,7 @@ class HyperOptResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [ abs_paths = [
current_path.parent.parent.joinpath('user_data/hyperopts'), Path.cwd().joinpath('user_data/hyperopts'),
current_path, current_path,
] ]
@ -77,3 +78,66 @@ class HyperOptResolver(IResolver):
f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist "
"or contains Python code errors." "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 = [
Path.cwd().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."
)

View File

@ -2,20 +2,25 @@
import os import os
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
from filelock import Timeout
import pandas as pd import pandas as pd
import pytest import pytest
from arrow import Arrow
from filelock import Timeout
from freqtrade import DependencyException from freqtrade import DependencyException
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.history import load_tickerdata_file 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 import setup_configuration, start_hyperopt from freqtrade.optimize import setup_configuration, start_hyperopt
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver 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
from freqtrade.state import RunMode 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) patched_configuration_load_config_file)
@ -25,6 +30,21 @@ def hyperopt(default_conf, mocker):
return Hyperopt(default_conf) 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 # Functions for recurrent object patching
def create_trials(mocker, hyperopt) -> None: def create_trials(mocker, hyperopt) -> None:
""" """
@ -166,6 +186,18 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
assert hasattr(x, "ticker_interval") 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: def test_start(mocker, default_conf, caplog) -> None:
start_mock = MagicMock() start_mock = MagicMock()
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -254,26 +286,54 @@ def test_start_filelock(mocker, default_conf, caplog) -> None:
) )
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None:
hl = HyperOptLossResolver(default_conf).hyperoptloss
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) correct = hl.hyperopt_loss_function(hyperopt_results, 600)
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100)
under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20) under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100)
assert over > correct assert over > correct
assert under > correct assert under > correct
def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None: def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None:
shorter = hyperopt.calculate_loss(1, 100, 20) resultsb = hyperopt_results.copy()
longer = hyperopt.calculate_loss(1, 100, 30) resultsb['trade_duration'][1] = 20
hl = HyperOptLossResolver(default_conf).hyperoptloss
longer = hl.hyperopt_loss_function(hyperopt_results, 100)
shorter = hl.hyperopt_loss_function(resultsb, 100)
assert shorter < longer assert shorter < longer
def test_loss_calculation_has_limited_profit(hyperopt) -> None: def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None:
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) results_over = hyperopt_results.copy()
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) results_under = hyperopt_results.copy()
assert over == correct results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
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(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
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 assert under > correct
@ -371,6 +431,11 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for tickerdata, once to save evaluations
assert dumper.call_count == 2 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']
assert hasattr(hyperopt, "position_stacking")
def test_format_results(hyperopt): def test_format_results(hyperopt):
@ -468,7 +533,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
) )
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe', '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) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
@ -510,3 +575,36 @@ def test_generate_optimizer(mocker, default_conf) -> None:
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
assert generate_optimizer_value == response_expected 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())
Hyperopt(default_conf)
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)

View File

@ -1,18 +1,18 @@
# 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'
# This class is a sample. Feel free to customize it. # This class is a sample. Feel free to customize it.
class SampleHyperOpts(IHyperOpt): class SampleHyperOpts(IHyperOpt):

View File

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