Skip to main content

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.

CPU Core Requirement

Hyperopt can crash when used with only 1 CPU Core as found out in Issue #1133

Strategy-Based Hyperopt

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.

Raspberry Pi

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
Indicator Requirements

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 optimization
  • generate_roi_table - for custom ROI optimization
  • stoploss_space - for custom stoploss optimization
  • trailing_space - for custom trailing stop optimization
  • max_open_trades_space - for custom max_open_trades optimization
Quick ROI, Stoploss and Trailing 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.

  1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10.
  2. 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".
Guards and Triggers

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 defining space='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.

Parameter Space Assignment

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

Indicator Requirements

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 of RealParameter 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 for CategoricalParameter([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 to False, the parameter will not be included in optimization process. (Default: True)
  • load - when set to False, results of a previous hyperopt run (in buy_params and sell_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)
Effects of load=False on Backtesting

Be 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 in populate_indicators

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.

Range Property Behavior

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 Availability

range property may also be used with DecimalParameter and CategoricalParameter. RealParameter does not provide this property due to infinite search space.

Performance Tip

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 and ema_long calculations from populate_indicators() to populate_entry_trend(). Since populate_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 of populate_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 ratio
  • SharpeHyperOptLossDaily - Optimizes Sharpe ratio calculated on daily returns
  • SortinoHyperOptLoss - Optimizes Sortino ratio
  • SortinoHyperOptLossDaily - Optimizes Sortino ratio calculated on daily returns
  • CalmarHyperOptLoss - Optimizes Calmar ratio
  • MaxDrawDownHyperOptLoss - Minimizes maximum drawdown
  • MaxDrawDownRelativeHyperOptLoss - Minimizes maximum relative drawdown
  • ProfitLoss - Maximizes profit
  • OnlyProfitHyperOptLoss - 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

  1. Test your strategy first - Ensure it works in backtesting
  2. Start with small parameter ranges - Reduce search space
  3. Use meaningful parameters - Don't optimize everything
  4. Consider market conditions - Use appropriate time ranges

Optimization Process

  1. Start with few epochs - Test the setup (10-50 epochs)
  2. Gradually increase epochs - More epochs = better results
  3. Use multiple CPU cores - Speed up the process
  4. Monitor progress - Check intermediate results

Result Analysis

  1. Check multiple metrics - Don't rely on loss function alone
  2. Validate with out-of-sample data - Test on different periods
  3. Consider robustness - Small parameter changes shouldn't drastically change results
  4. 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

Next Steps

After successful hyperoptimization:

  1. Backtesting - Validate optimized parameters
  2. Paper Trading - Test with live data
  3. Advanced Hyperopt - Complex optimization techniques
  4. Strategy Analysis - Analyze performance