This page explains some advanced Hyperopt topics that may require higher coding skills and Python knowledge than creation of an ordinal hyperoptimization class.
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 function 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 in userdata/hyperopts.
from datetime import datetime from typing import Any, Dict from pandas import DataFrame 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, config: Dict, processed: Dict[str, DataFrame], backtest_stats: Dict[str, Any], *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_ratio'].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 resulting trades. The following columns are available in results (corresponds to the output-file of backtesting when used with
pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs
trade_count: Amount of trades (identical to
min_date: Start date of the timerange used
min_date: End date of the timerange used
config: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
processed: Dict of Dataframes with the pair as keys containing the data used for backtesting.
backtest_stats: Backtesting statistics using the same format as the backtesting file "strategy" substructure. Available fields can be seen in
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.
This function is called once per epoch - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
Please keep the arguments
**kwargs in the interface to allow us to extend this interface in the future.
Overriding pre-defined spaces¶
To override a pre-defined space (
trailing_space), define a nested class called Hyperopt and define the required spaces as follows:
class MyAwesomeStrategy(IStrategy): class HyperOpt: # Define a custom stoploss space. def stoploss_space(): return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')] # Define custom ROI space def roi_space() -> List[Dimension]: return [ Integer(10, 120, name='roi_t1'), Integer(10, 60, name='roi_t2'), Integer(10, 40, name='roi_t3'), SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'), SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'), SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), ]
All overrides are optional and can be mixed/matched as necessary.
Overriding Base estimator¶
You can define your own estimator for Hyperopt by implementing
generate_estimator() in the Hyperopt subclass.
class MyAwesomeStrategy(IStrategy): class HyperOpt: def generate_estimator(): return "RF"
Possible values are either one of "GP", "RF", "ET", "GBRT" (Details can be found in the scikit-optimize documentation), or "an instance of a class that inherits from
RegressorMixin (from sklearn) and where the
predict method has an optional
return_std argument, which returns
std(Y | x) along with
E[Y | x]".
Some research will be necessary to find additional Regressors.
ExtraTreesRegressor ("ET") with additional parameters:
class MyAwesomeStrategy(IStrategy): class HyperOpt: def generate_estimator(): from skopt.learning import ExtraTreesRegressor # Corresponds to "ET" - but allows additional parameters. return ExtraTreesRegressor(n_estimators=100)
While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used.
If you're unsure about this, best use one of the Defaults (
"ET" has proven to be the most versatile) without further parameters.
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
Categorical- Pick from a list of categories (e.g.
Categorical(['a', 'b', 'c'], name="cat"))
Integer- Pick from a range of whole numbers (e.g.
Integer(1, 10, name='rsi'))
SKDecimal- Pick from a range of decimal numbers with limited precision (e.g.
SKDecimal(0.1, 0.5, decimals=3, name='adx')). Available only with freqtrade.
Real- Pick from a range of decimal numbers with full precision (e.g.
Real(0.1, 0.5, name='adx')
You can import all of these from
Real are only aliases for their corresponding scikit-optimize Spaces.
SKDecimal is provided by freqtrade for faster optimizations.
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa
SKDecimal vs. Real
We recommend to use
SKDecimal instead of the
Real space in almost all cases. While the Real space provides full accuracy (up to ~16 decimal places) - this precision is rarely needed, and leads to unnecessary long hyperopt times.
Assuming the definition of a rather small space (
SKDecimal(0.10, 0.15, decimals=2, name='xxx')) - SKDecimal will have 5 possibilities (
[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]).
A corresponding real space
Real(0.10, 0.15 name='xxx') on the other hand has an almost unlimited number of possibilities (
[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]).