Hyperopt
This page explains how to tune your strategy by finding the optimal parameters, a process called hyperparameter optimization. The bot uses algorithms included in the optuna
package to accomplish this.
The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time.
In general, the search for best parameters starts with a few random combinations (see below for more details) and then uses one of optuna's sampler algorithms (currently NSGAIIISampler) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the loss function.
Hyperopt requires historic data to be available, just as backtesting does (hyperopt runs backtesting many times with different parameters).
To learn how to get data for the pairs and exchange you're interested in, head over to the Data Downloading section of the documentation.
Hyperopt can crash when used with only 1 CPU Core as found out in Issue #1133
Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy.
The legacy method was supported up to 2021.8 and has been removed in 2021.9.
Install Hyperopt Dependencies
Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below.
Since Hyperopt is a resource intensive process, running it on a Raspberry Pi is not recommended nor supported.
Docker
The docker-image includes hyperopt dependencies, no further action needed.
Easy Installation Script (setup.sh) / Manual Installation
source .venv/bin/activate
pip install -r requirements-hyperopt.txt
Hyperopt Command Reference
Basic Commands
# Basic hyperopt run
freqtrade hyperopt --strategy MyStrategy --hyperopt-loss SharpeHyperOptLoss --epochs 100
# Optimize specific spaces
freqtrade hyperopt --strategy MyStrategy --spaces buy sell roi stoploss --epochs 100
# Use multiple CPU cores
freqtrade hyperopt --strategy MyStrategy -j 4 --epochs 100
# Continue from previous results
freqtrade hyperopt --strategy MyStrategy --epochs 100 --hyperopt-filename hyperopt_results.pickle
Advanced Options
# Optimize with specific timerange
freqtrade hyperopt --strategy MyStrategy --timerange 20230101-20230601 --epochs 100
# Use specific pairs
freqtrade hyperopt --strategy MyStrategy --pairs BTC/USDT ETH/USDT --epochs 100
# Enable position stacking
freqtrade hyperopt --strategy MyStrategy --enable-position-stacking --epochs 100
# Analyze per epoch (memory efficient)
freqtrade hyperopt --strategy MyStrategy --analyze-per-epoch --epochs 100
Hyperopt Checklist
Checklist on all tasks/possibilities in hyperopt
Depending on the space you want to optimize, only some of the below are required:
- Define parameters with
space='buy'
- for entry signal optimization - Define parameters with
space='sell'
- for exit signal optimization
populate_indicators
needs to create all indicators any of the spaces may use, otherwise hyperopt will not work.
Rarely you may also need to create a nested class named HyperOpt
and implement:
roi_space
- for custom ROI optimizationgenerate_roi_table
- for custom ROI optimizationstoploss_space
- for custom stoploss optimizationtrailing_space
- for custom trailing stop optimizationmax_open_trades_space
- for custom max_open_trades optimization
You can quickly optimize the spaces roi
, stoploss
and trailing
without changing anything in your strategy.
# Have a working strategy at hand.
freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100
Hyperopt Execution Logic
Hyperopt will first load your data into memory and will then run populate_indicators()
once per Pair to generate all indicators, unless --analyze-per-epoch
is specified.
Hyperopt will then spawn into different processes (number of processors, or -j <n>
), and run backtesting over and over again, changing the parameters that are part of the --spaces
defined.
For every new set of parameters, freqtrade will run first populate_entry_trend()
followed by populate_exit_trend()
, and then run the regular backtesting process to simulate trades.
After backtesting, the results are passed into the loss function, which will evaluate if this result was better or worse than previous results.
Based on the loss function result, hyperopt will determine the next set of parameters to try in the next round of backtesting.
Configure Your Guards and Triggers
There are two places you need to change in your strategy file to add a new buy hyperopt for testing:
- Define the parameters at the class level hyperopt shall be optimizing.
- Within
populate_entry_trend()
- use defined parameter values instead of raw constants.
There you have two different types of indicators: 1. guards
and 2. triggers
.
- Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10.
- Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band".
Technically, there is no difference between Guards and Triggers.
However, this guide will make this distinction to make it clear that signals should not be "sticking".
Sticking signals are signals that are active for multiple candles. This can lead into entering a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards.
Exit Signal Optimization
Similar to the entry-signal above, exit-signals can also be optimized. Place the corresponding settings into the following methods:
- Define the parameters at the class level hyperopt shall be optimizing, either naming them
sell_*
, or by explicitly definingspace='sell'
. - Within
populate_exit_trend()
- use defined parameter values instead of raw constants.
The configuration and rules are the same than for buy signals.
Solving a Mystery
Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your long entries.
And you also wonder should you use RSI or ADX to help with those decisions.
If you decide to use RSI or ADX, which values should I use for them?
So let's use hyperparameter optimization to solve this mystery.
Defining Indicators to be Used
We start by calculating the indicators our strategy is going to use.
class MyAwesomeStrategy(IStrategy):
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Generate all indicators used by the strategy
"""
dataframe['adx'] = ta.ADX(dataframe)
dataframe['rsi'] = ta.RSI(dataframe)
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe['bb_lowerband'] = bollinger['lowerband']
dataframe['bb_middleband'] = bollinger['middleband']
dataframe['bb_upperband'] = bollinger['upperband']
return dataframe
Hyperoptable Parameters
We continue to define hyperoptable parameters:
class MyAwesomeStrategy(IStrategy):
buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy")
buy_rsi = IntParameter(20, 40, default=30, space="buy")
buy_adx_enabled = BooleanParameter(default=True, space="buy")
buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy")
buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy")
The above definition says: I have five parameters I want to randomly combine to find the best combination.
buy_rsi
is an integer parameter, which will be tested between 20 and 40. This space has a size of 20.
buy_adx
is a decimal parameter, which will be evaluated between 20 and 40 with 1 decimal place (so values are 20.1, 20.2, ...). This space has a size of 200.
Then we have three category variables. First two are either True
or False
.
We use these to either enable or disable the ADX and RSI guards.
The last one we call trigger
and use it to decide which buy trigger we want to use.
Parameters must either be assigned to a variable named buy_*
or sell_*
- or contain space='buy'
| space='sell'
to be assigned to a space correctly.
If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt.
Parameters with unclear space (e.g. adx_period = IntParameter(4, 24, default=14)
- no explicit nor implicit space) will not be detected and will therefore be ignored.
So let's write the buy strategy using these values:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if self.buy_adx_enabled.value:
conditions.append(dataframe['adx'] > self.buy_adx.value)
if self.buy_rsi_enabled.value:
conditions.append(dataframe['rsi'] < self.buy_rsi.value)
# TRIGGERS
if self.buy_trigger.value == 'bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if self.buy_trigger.value == 'macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal']
))
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'enter_long'] = 1
return dataframe
Hyperopt will now call populate_entry_trend()
many times (epochs
) with different value combinations.
It will use the given historical data and simulate buys based on the buy signals generated with the above function.
Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured loss function).
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 add it to the populate_indicators()
method in your strategy or hyperopt file.
Parameter Types
There are four parameter types each suited for different purposes.
IntParameter
- defines an integral parameter with upper and lower boundaries of search space.DecimalParameter
- defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead ofRealParameter
in most cases.RealParameter
- defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities.CategoricalParameter
- defines a parameter with a predetermined number of choices.BooleanParameter
- Shorthand forCategoricalParameter([True, False])
- great for "enable" parameters.
Parameter Options
There are two parameter options that can help you to quickly test various ideas:
optimize
- when set toFalse
, the parameter will not be included in optimization process. (Default: True)load
- when set toFalse
, results of a previous hyperopt run (inbuy_params
andsell_params
either in your strategy or the JSON output file) will not be used as the starting value for subsequent hyperopts. The default value specified in the parameter will be used instead. (Default: True)
load=False
on BacktestingBe aware that setting the load
option to False
will mean backtesting will also use the default value specified in the parameter and not the value found through hyperoptimisation.
Hyperoptable parameters cannot be used in populate_indicators
- as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
Optimizing an Indicator Parameter
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
By default, we assume a stoploss of 5% - and a take-profit (minimal_roi
) of 10% - which means freqtrade will sell the trade once 10% profit has been reached.
from pandas import DataFrame
from functools import reduce
import talib.abstract as ta
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IStrategy, IntParameter)
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyAwesomeStrategy(IStrategy):
stoploss = -0.05
timeframe = '15m'
minimal_roi = {
"0": 0.10
}
# Define the parameter spaces
buy_ema_short = IntParameter(3, 50, default=5)
buy_ema_long = IntParameter(15, 200, default=50)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Generate all indicators used by the strategy"""
# Calculate all ema_short values
for val in self.buy_ema_short.range:
dataframe[f'ema_short_{val}'] = ta.EMA(dataframe, timeperiod=val)
# Calculate all ema_long values
for val in self.buy_ema_long.range:
dataframe[f'ema_long_{val}'] = ta.EMA(dataframe, timeperiod=val)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
conditions.append(qtpylib.crossed_above(
dataframe[f'ema_short_{self.buy_ema_short.value}'],
dataframe[f'ema_long_{self.buy_ema_long.value}']
))
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
conditions.append(qtpylib.crossed_above(
dataframe[f'ema_long_{self.buy_ema_long.value}'],
dataframe[f'ema_short_{self.buy_ema_short.value}']
))
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'exit_long'] = 1
return dataframe
Breaking it down:
Using self.buy_ema_short.range
will return a range object containing all entries between the Parameters low and high value.
In this case (IntParameter(3, 50, default=5)
), the loop would run for all numbers between 3 and 50 ([3, 4, 5, ... 49, 50]
).
By using this in a loop, hyperopt will generate 48 new columns (['ema_short_3', 'ema_short_4', ... , 'ema_short_50']
).
Hyperopt itself will then use the selected value to create the buy and sell signals.
While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters.
self.buy_ema_short.range
will act differently between hyperopt and other modes. For hyperopt, the above example may generate 48 new columns, however for all other modes (backtesting, dry/live), it will only generate the column for the selected value. You should therefore avoid using the resulting column with explicit values (values other than self.buy_ema_short.value
).
range
property may also be used with DecimalParameter
and CategoricalParameter
. RealParameter
does not provide this property due to infinite search space.
During normal hyperopting, indicators are calculated once and supplied to each epoch, linearly increasing RAM usage as a factor of increasing cores. As this also has performance implications, there are two alternatives to reduce RAM usage:
- Move
ema_short
andema_long
calculations frompopulate_indicators()
topopulate_entry_trend()
. Sincepopulate_entry_trend()
will be calculated every epoch, you don't need to use.range
functionality. - hyperopt provides
--analyze-per-epoch
which will move the execution ofpopulate_indicators()
to the epoch process, calculating a single value per parameter per epoch instead of using the.range
functionality. In this case,.range
functionality will only return the actually used value.
These alternatives will reduce RAM usage, but increase CPU usage. However, your hyperopting run will be less likely to fail due to Out Of Memory (OOM) issues.
Whether you are using .range
functionality or the alternatives above, you should try to use space ranges as small as possible since this will improve CPU/RAM usage.
Loss Functions
Hyperopt uses loss functions to determine which parameter combination performs best. Freqtrade provides several built-in loss functions:
Available Loss Functions
SharpeHyperOptLoss
- Optimizes Sharpe ratioSharpeHyperOptLossDaily
- Optimizes Sharpe ratio calculated on daily returnsSortinoHyperOptLoss
- Optimizes Sortino ratioSortinoHyperOptLossDaily
- Optimizes Sortino ratio calculated on daily returnsCalmarHyperOptLoss
- Optimizes Calmar ratioMaxDrawDownHyperOptLoss
- Minimizes maximum drawdownMaxDrawDownRelativeHyperOptLoss
- Minimizes maximum relative drawdownProfitLoss
- Maximizes profitOnlyProfitHyperOptLoss
- Optimizes profit while ensuring profitability
Using Loss Functions
# Use Sharpe ratio optimization
freqtrade hyperopt --strategy MyStrategy --hyperopt-loss SharpeHyperOptLoss --epochs 100
# Use profit optimization
freqtrade hyperopt --strategy MyStrategy --hyperopt-loss ProfitLoss --epochs 100
Best Practices
Strategy Preparation
- Test your strategy first - Ensure it works in backtesting
- Start with small parameter ranges - Reduce search space
- Use meaningful parameters - Don't optimize everything
- Consider market conditions - Use appropriate time ranges
Optimization Process
- Start with few epochs - Test the setup (10-50 epochs)
- Gradually increase epochs - More epochs = better results
- Use multiple CPU cores - Speed up the process
- Monitor progress - Check intermediate results
Result Analysis
- Check multiple metrics - Don't rely on loss function alone
- Validate with out-of-sample data - Test on different periods
- Consider robustness - Small parameter changes shouldn't drastically change results
- Document your findings - Keep track of what works
Troubleshooting
Common Issues
Out of Memory (OOM) errors:
- Use
--analyze-per-epoch
- Reduce parameter ranges
- Use fewer CPU cores
- Reduce dataset size
Slow performance:
- Use fewer pairs
- Reduce timerange
- Optimize parameter ranges
- Use faster hardware
No improvement:
- Check strategy logic
- Verify parameter ranges
- Try different loss functions
- Increase epochs
Getting Help
- Discord: Join the Freqtrade Discord
- GitHub: Report hyperopt issues
- Documentation: Advanced Hyperopt
Next Steps
After successful hyperoptimization:
- Backtesting - Validate optimized parameters
- Paper Trading - Test with live data
- Advanced Hyperopt - Complex optimization techniques
- Strategy Analysis - Analyze performance