Merge pull request #930 from freqtrade/skopt
Replace Hyperopt with scikit-optimize
This commit is contained in:
commit
d8d0579c5a
300
docs/hyperopt.md
300
docs/hyperopt.md
@ -1,155 +1,114 @@
|
||||
# Hyperopt
|
||||
This page explains how to tune your strategy by finding the optimal
|
||||
parameters with Hyperopt.
|
||||
This page explains how to tune your strategy by finding the optimal
|
||||
parameters, a process called hyperparameter optimization. The bot uses several
|
||||
algorithms included in the `scikit-optimize` 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.
|
||||
|
||||
## Table of Contents
|
||||
- [Prepare your Hyperopt](#prepare-hyperopt)
|
||||
- [1. Configure your Guards and Triggers](#1-configure-your-guards-and-triggers)
|
||||
- [2. Update the hyperopt config file](#2-update-the-hyperopt-config-file)
|
||||
- [Advanced Hyperopt notions](#advanced-notions)
|
||||
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
|
||||
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
|
||||
- [Solving a Mystery](#solving-a-mystery)
|
||||
- [Adding New Indicators](#adding-new-indicators)
|
||||
- [Execute Hyperopt](#execute-hyperopt)
|
||||
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
||||
|
||||
## Prepare Hyperopt
|
||||
Before we start digging in Hyperopt, we recommend you to take a look at
|
||||
your strategy file located into [user_data/strategies/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||
## Prepare Hyperopting
|
||||
We recommend you start by taking a look at `hyperopt.py` file located in [freqtrade/optimize](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
|
||||
|
||||
### 1. Configure your Guards and Triggers
|
||||
There are two places you need to change in your strategy file to add a
|
||||
new buy strategy for testing:
|
||||
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
||||
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
||||
### Configure your Guards and Triggers
|
||||
There are two places you need to change to add a new buy strategy for testing:
|
||||
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L278-L294).
|
||||
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L218-L229)
|
||||
and the associated methods `indicator_space`, `roi_space`, `stoploss_space`.
|
||||
|
||||
There you have two different type 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.
|
||||
There you have two different type 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.
|
||||
"buy when EMA5 crosses over EMA10" or "buy when close price touches lower
|
||||
bollinger band".
|
||||
|
||||
HyperOpt will, for each eval round, pick just ONE trigger, and possibly
|
||||
multiple guards. So that the constructed strategy will be something like
|
||||
Hyperoptimization will, for each eval round, pick one trigger and possibly
|
||||
multiple guards. The constructed strategy will be something like
|
||||
"*buy exactly when close price touches lower bollinger band, BUT only if
|
||||
ADX > 10*".
|
||||
|
||||
|
||||
If you have updated the buy strategy, means change the content of
|
||||
If you have updated the buy strategy, ie. changed the contents of
|
||||
`populate_buy_trend()` method you have to update the `guards` and
|
||||
`triggers` hyperopts must used.
|
||||
`triggers` hyperopts must use.
|
||||
|
||||
As for an example if your `populate_buy_trend()` method is:
|
||||
```python
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(dataframe['rsi'] < 35) &
|
||||
(dataframe['adx'] > 65),
|
||||
'buy'] = 1
|
||||
## Solving a Mystery
|
||||
|
||||
return dataframe
|
||||
```
|
||||
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
||||
Bands to trigger your buys. And you also wonder should you use RSI or ADX to
|
||||
help with those buy 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.
|
||||
|
||||
Your hyperopt file must contain `guards` to find the right value for
|
||||
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
|
||||
means you will need to enable/disable triggers.
|
||||
|
||||
In our case the `SPACE` and `populate_buy_trend` in your strategy file
|
||||
will look like:
|
||||
```python
|
||||
space = {
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||
]),
|
||||
'adx': hp.choice('adx', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
|
||||
]),
|
||||
'trigger': hp.choice('trigger', [
|
||||
{'type': 'lower_bb'},
|
||||
{'type': 'faststoch10'},
|
||||
{'type': 'ao_cross_zero'},
|
||||
{'type': 'ema5_cross_ema10'},
|
||||
{'type': 'macd_cross_signal'},
|
||||
{'type': 'sar_reversal'},
|
||||
{'type': 'stochf_cross'},
|
||||
{'type': 'ht_sine'},
|
||||
]),
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if params['adx']['enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||
if params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
|
||||
# TRIGGERS
|
||||
triggers = {
|
||||
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
||||
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
|
||||
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
||||
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
||||
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
|
||||
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
|
||||
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
|
||||
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
### 2. Update the hyperopt config file
|
||||
Hyperopt is using a dedicated config file. Currently hyperopt
|
||||
cannot use your config file. It is also made on purpose to allow you
|
||||
testing your strategy with different configurations.
|
||||
|
||||
The Hyperopt configuration is located in
|
||||
[user_data/hyperopt_conf.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
||||
|
||||
|
||||
## Advanced notions
|
||||
### Understand the Guards and Triggers
|
||||
When you need to add the new guards and triggers to be hyperopt
|
||||
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
||||
|
||||
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
||||
|
||||
If it's a guard, you will add a line like this:
|
||||
```
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||
]),
|
||||
```
|
||||
This says, "*one of the guards is RSI, it can have two values, enabled or
|
||||
disabled. If it is enabled, try different values for it between 20 and 40*".
|
||||
|
||||
So, the part of the strategy builder using the above setting looks like
|
||||
this:
|
||||
We will start by defining a search space:
|
||||
|
||||
```
|
||||
if params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching strategy parameters
|
||||
"""
|
||||
return [
|
||||
Integer(20, 40, name='adx-value'),
|
||||
Integer(20, 40, name='rsi-value'),
|
||||
Categorical([True, False], name='adx-enabled'),
|
||||
Categorical([True, False], name='rsi-enabled'),
|
||||
Categorical(['bb_lower', 'macd_cross_signal'], name='trigger')
|
||||
]
|
||||
```
|
||||
|
||||
It checks if Hyperopt wants the RSI guard to be enabled for this
|
||||
round `params['rsi']['enabled']` and if it is, then it will add a
|
||||
condition that says RSI must be smaller than the value hyperopt picked
|
||||
for this evaluation, which is given in the `params['rsi']['value']`.
|
||||
Above definition says: I have five parameters I want you to randomly combine
|
||||
to find the best combination. Two of them are integer values (`adx-value`
|
||||
and `rsi-value`) and I want you test in the range of values 20 to 40.
|
||||
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.
|
||||
|
||||
That's it. Now you can add new parts of strategies to Hyperopt and it
|
||||
will try all the combinations with all different values in the search
|
||||
for best working algo.
|
||||
So let's write the buy strategy using these values:
|
||||
|
||||
```
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if 'adx-enabled' in params and params['adx-enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||
|
||||
### Add a new Indicators
|
||||
If you want to test an indicator that isn't used by the bot currently,
|
||||
you need to add it to the `populate_indicators()` method in `hyperopt.py`.
|
||||
# TRIGGERS
|
||||
if params['trigger'] == 'bb_lower':
|
||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||
if params['trigger'] == 'macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macd'], dataframe['macdsignal']
|
||||
))
|
||||
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_buy_trend
|
||||
```
|
||||
|
||||
Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`)
|
||||
with different value combinations. It will then use the given historical data and make
|
||||
buys based on the buy signals generated with the above function and based on the results
|
||||
it will end with telling you which paramter combination produced the best profits.
|
||||
|
||||
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
|
||||
that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`.
|
||||
|
||||
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 `hyperopt.py`.
|
||||
|
||||
## Execute Hyperopt
|
||||
Once you have updated your hyperopt configuration you can run it.
|
||||
@ -164,12 +123,12 @@ python3 ./freqtrade/main.py -c config.json hyperopt -e 5000
|
||||
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
||||
running at least several thousand evaluations.
|
||||
|
||||
### Execute hyperopt with different ticker-data source
|
||||
### Execute Hyperopt with Different Ticker-Data Source
|
||||
If you would like to hyperopt parameters using an alternate ticker data that
|
||||
you have on-disk, use the `--datadir PATH` option. Default hyperopt will
|
||||
use data from directory `user_data/data`.
|
||||
|
||||
### Running hyperopt with smaller testset
|
||||
### Running Hyperopt with Smaller Testset
|
||||
Use the `--timeperiod` argument to change how much of the testset
|
||||
you want to use. The last N ticks/timeframes will be used.
|
||||
Example:
|
||||
@ -178,7 +137,7 @@ Example:
|
||||
python3 ./freqtrade/main.py hyperopt --timeperiod -200
|
||||
```
|
||||
|
||||
### Running hyperopt with smaller search space
|
||||
### Running Hyperopt with Smaller Search Space
|
||||
Use the `--spaces` argument to limit the search space used by hyperopt.
|
||||
Letting Hyperopt optimize everything is a huuuuge search space. Often it
|
||||
might make more sense to start by just searching for initial buy algorithm.
|
||||
@ -193,87 +152,44 @@ Legal values are:
|
||||
- `stoploss`: search for the best stoploss value
|
||||
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||
|
||||
## Understand the hyperopts result
|
||||
Once Hyperopt is completed you can use the result to adding new buy
|
||||
signal. Given following result from hyperopt:
|
||||
```
|
||||
Best parameters:
|
||||
{
|
||||
"adx": {
|
||||
"enabled": true,
|
||||
"value": 15.0
|
||||
},
|
||||
"fastd": {
|
||||
"enabled": true,
|
||||
"value": 40.0
|
||||
},
|
||||
"green_candle": {
|
||||
"enabled": true
|
||||
},
|
||||
"mfi": {
|
||||
"enabled": false
|
||||
},
|
||||
"over_sar": {
|
||||
"enabled": false
|
||||
},
|
||||
"rsi": {
|
||||
"enabled": true,
|
||||
"value": 37.0
|
||||
},
|
||||
"trigger": {
|
||||
"type": "lower_bb"
|
||||
},
|
||||
"uptrend_long_ema": {
|
||||
"enabled": true
|
||||
},
|
||||
"uptrend_short_ema": {
|
||||
"enabled": false
|
||||
},
|
||||
"uptrend_sma": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
## Understand the Hyperopts Result
|
||||
Once Hyperopt is completed you can use the result to create a new strategy.
|
||||
Given the following result from hyperopt:
|
||||
|
||||
Best Result:
|
||||
2197 trades. Avg profit 1.84%. Total profit 0.79367541 BTC. Avg duration 241.0 mins.
|
||||
```
|
||||
Best result:
|
||||
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
||||
with values:
|
||||
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower'}
|
||||
```
|
||||
|
||||
You should understand this result like:
|
||||
- You should **consider** the guard "adx" (`"adx"` is `"enabled": true`)
|
||||
and the best value is `15.0` (`"value": 15.0,`)
|
||||
- You should **consider** the guard "fastd" (`"fastd"` is `"enabled":
|
||||
true`) and the best value is `40.0` (`"value": 40.0,`)
|
||||
- You should **consider** to enable the guard "green_candle"
|
||||
(`"green_candle"` is `"enabled": true`) but this guards as no
|
||||
customizable value.
|
||||
- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
|
||||
- and so on...
|
||||
- The buy trigger that worked best was `bb_lower`.
|
||||
- You should not use ADX because `adx-enabled: False`)
|
||||
- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
|
||||
|
||||
You have to look inside your strategy file into `buy_strategy_generator()`
|
||||
method, what those values match to.
|
||||
|
||||
So for example you had `adx:` with the `value: 15.0` so we would look
|
||||
at `adx`-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:
|
||||
```
|
||||
(dataframe['adx'] > 15.0)
|
||||
(dataframe['rsi'] < 29.0)
|
||||
```
|
||||
|
||||
Translating your whole hyperopt result to as the new buy-signal
|
||||
would be the following:
|
||||
Translating your whole hyperopt result as the new buy-signal
|
||||
would then look like:
|
||||
```
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 15.0) & # adx-value
|
||||
(dataframe['fastd'] < 40.0) & # fastd-value
|
||||
(dataframe['close'] > dataframe['open']) & # green_candle
|
||||
(dataframe['rsi'] < 37.0) & # rsi-value
|
||||
(dataframe['ema50'] > dataframe['ema100']) # uptrend_long_ema
|
||||
(dataframe['rsi'] < 29.0) & # rsi-value
|
||||
dataframe['close'] < dataframe['bb_lowerband'] # trigger
|
||||
),
|
||||
'buy'] = 1
|
||||
return dataframe
|
||||
```
|
||||
|
||||
## Next step
|
||||
## Next Step
|
||||
Now you have a perfect bot and want to control it from Telegram. Your
|
||||
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
||||
|
@ -4,22 +4,21 @@
|
||||
This module contains the hyperopt logic
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import pickle
|
||||
import signal
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from functools import reduce
|
||||
from math import exp
|
||||
from operator import itemgetter
|
||||
from typing import Dict, Any, Callable, Optional
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
import numpy
|
||||
import talib.abstract as ta
|
||||
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
|
||||
from pandas import DataFrame
|
||||
from sklearn.externals.joblib import Parallel, delayed, dump, load
|
||||
from skopt import Optimizer
|
||||
from skopt.space import Categorical, Dimension, Integer, Real
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.arguments import Arguments
|
||||
@ -29,6 +28,9 @@ from freqtrade.optimize.backtesting import Backtesting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
||||
TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl')
|
||||
|
||||
|
||||
class Hyperopt(Backtesting):
|
||||
"""
|
||||
@ -44,7 +46,6 @@ class Hyperopt(Backtesting):
|
||||
# to the number of days
|
||||
self.target_trades = 600
|
||||
self.total_tries = config.get('epochs', 0)
|
||||
self.current_tries = 0
|
||||
self.current_best_loss = 100
|
||||
|
||||
# max average trade duration in minutes
|
||||
@ -56,130 +57,38 @@ class Hyperopt(Backtesting):
|
||||
# check that the reported Σ% values do not exceed this!
|
||||
self.expected_max_profit = 3.0
|
||||
|
||||
# Configuration and data used by hyperopt
|
||||
self.processed: Optional[Dict[str, Any]] = None
|
||||
# Previous evaluations
|
||||
self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle')
|
||||
self.trials: List = []
|
||||
|
||||
# Hyperopt Trials
|
||||
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
||||
self.trials = Trials()
|
||||
def get_args(self, params):
|
||||
dimensions = self.hyperopt_space()
|
||||
# Ensure the number of dimensions match
|
||||
# the number of parameters in the list x.
|
||||
if len(params) != len(dimensions):
|
||||
raise ValueError('Mismatch in number of search-space dimensions. '
|
||||
f'len(dimensions)=={len(dimensions)} and len(x)=={len(params)}')
|
||||
|
||||
# Create a dict where the keys are the names of the dimensions
|
||||
# and the values are taken from the list of parameters x.
|
||||
arg_dict = {dim.name: value for dim, value in zip(dimensions, params)}
|
||||
return arg_dict
|
||||
|
||||
@staticmethod
|
||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
"""
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
# SAR Parabolic
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
|
||||
return dataframe
|
||||
|
||||
@ -187,15 +96,16 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
Save hyperopt trials to file
|
||||
"""
|
||||
logger.info('Saving Trials to \'%s\'', self.trials_file)
|
||||
pickle.dump(self.trials, open(self.trials_file, 'wb'))
|
||||
if self.trials:
|
||||
logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file)
|
||||
dump(self.trials, self.trials_file)
|
||||
|
||||
def read_trials(self) -> Trials:
|
||||
def read_trials(self) -> List:
|
||||
"""
|
||||
Read hyperopt trials file
|
||||
"""
|
||||
logger.info('Reading Trials from \'%s\'', self.trials_file)
|
||||
trials = pickle.load(open(self.trials_file, 'rb'))
|
||||
trials = load(self.trials_file)
|
||||
os.remove(self.trials_file)
|
||||
return trials
|
||||
|
||||
@ -203,9 +113,15 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
Display Best hyperopt result
|
||||
"""
|
||||
vals = json.dumps(self.trials.best_trial['misc']['vals'], indent=4)
|
||||
results = self.trials.best_trial['result']['result']
|
||||
logger.info('Best result:\n%s\nwith values:\n%s', results, vals)
|
||||
results = sorted(self.trials, key=itemgetter('loss'))
|
||||
best_result = results[0]
|
||||
logger.info(
|
||||
'Best result:\n%s\nwith values:\n%s',
|
||||
best_result['result'],
|
||||
best_result['params']
|
||||
)
|
||||
if 'roi_t1' in best_result['params']:
|
||||
logger.info('ROI table:\n%s', self.generate_roi_table(best_result['params']))
|
||||
|
||||
def log_results(self, results) -> None:
|
||||
"""
|
||||
@ -231,7 +147,8 @@ class Hyperopt(Backtesting):
|
||||
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)
|
||||
return trade_loss + profit_loss + duration_loss
|
||||
result = trade_loss + profit_loss + duration_loss
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||
@ -247,87 +164,44 @@ class Hyperopt(Backtesting):
|
||||
return roi_table
|
||||
|
||||
@staticmethod
|
||||
def roi_space() -> Dict[str, Any]:
|
||||
def roi_space() -> List[Dimension]:
|
||||
"""
|
||||
Values to search for each ROI steps
|
||||
"""
|
||||
return {
|
||||
'roi_t1': hp.quniform('roi_t1', 10, 120, 20),
|
||||
'roi_t2': hp.quniform('roi_t2', 10, 60, 15),
|
||||
'roi_t3': hp.quniform('roi_t3', 10, 40, 10),
|
||||
'roi_p1': hp.quniform('roi_p1', 0.01, 0.04, 0.01),
|
||||
'roi_p2': hp.quniform('roi_p2', 0.01, 0.07, 0.01),
|
||||
'roi_p3': hp.quniform('roi_p3', 0.01, 0.20, 0.01),
|
||||
}
|
||||
return [
|
||||
Integer(10, 120, name='roi_t1'),
|
||||
Integer(10, 60, name='roi_t2'),
|
||||
Integer(10, 40, name='roi_t3'),
|
||||
Real(0.01, 0.04, name='roi_p1'),
|
||||
Real(0.01, 0.07, name='roi_p2'),
|
||||
Real(0.01, 0.20, name='roi_p3'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def stoploss_space() -> Dict[str, Any]:
|
||||
def stoploss_space() -> List[Dimension]:
|
||||
"""
|
||||
Stoploss Value to search
|
||||
Stoploss search space
|
||||
"""
|
||||
return {
|
||||
'stoploss': hp.quniform('stoploss', -0.5, -0.02, 0.02),
|
||||
}
|
||||
return [
|
||||
Real(-0.5, -0.02, name='stoploss'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def indicator_space() -> Dict[str, Any]:
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching strategy parameters
|
||||
"""
|
||||
return {
|
||||
'macd_below_zero': hp.choice('macd_below_zero', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'mfi': hp.choice('mfi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('mfi-value', 10, 25, 5)}
|
||||
]),
|
||||
'fastd': hp.choice('fastd', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('fastd-value', 15, 45, 5)}
|
||||
]),
|
||||
'adx': hp.choice('adx', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('adx-value', 20, 50, 5)}
|
||||
]),
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 5)}
|
||||
]),
|
||||
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'over_sar': hp.choice('over_sar', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'green_candle': hp.choice('green_candle', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_sma': hp.choice('uptrend_sma', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'trigger': hp.choice('trigger', [
|
||||
{'type': 'lower_bb'},
|
||||
{'type': 'lower_bb_tema'},
|
||||
{'type': 'faststoch10'},
|
||||
{'type': 'ao_cross_zero'},
|
||||
{'type': 'ema3_cross_ema10'},
|
||||
{'type': 'macd_cross_signal'},
|
||||
{'type': 'sar_reversal'},
|
||||
{'type': 'ht_sine'},
|
||||
{'type': 'heiken_reversal_bull'},
|
||||
{'type': 'di_cross'},
|
||||
]),
|
||||
}
|
||||
return [
|
||||
Integer(10, 25, name='mfi-value'),
|
||||
Integer(15, 45, name='fastd-value'),
|
||||
Integer(20, 50, name='adx-value'),
|
||||
Integer(20, 40, name='rsi-value'),
|
||||
Categorical([True, False], name='mfi-enabled'),
|
||||
Categorical([True, False], name='fastd-enabled'),
|
||||
Categorical([True, False], name='adx-enabled'),
|
||||
Categorical([True, False], name='rsi-enabled'),
|
||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
||||
]
|
||||
|
||||
def has_space(self, space: str) -> bool:
|
||||
"""
|
||||
@ -337,17 +211,17 @@ class Hyperopt(Backtesting):
|
||||
return True
|
||||
return False
|
||||
|
||||
def hyperopt_space(self) -> Dict[str, Any]:
|
||||
def hyperopt_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Return the space to use during Hyperopt
|
||||
"""
|
||||
spaces: Dict = {}
|
||||
spaces: List[Dimension] = []
|
||||
if self.has_space('buy'):
|
||||
spaces = {**spaces, **Hyperopt.indicator_space()}
|
||||
spaces += Hyperopt.indicator_space()
|
||||
if self.has_space('roi'):
|
||||
spaces = {**spaces, **Hyperopt.roi_space()}
|
||||
spaces += Hyperopt.roi_space()
|
||||
if self.has_space('stoploss'):
|
||||
spaces = {**spaces, **Hyperopt.stoploss_space()}
|
||||
spaces += Hyperopt.stoploss_space()
|
||||
return spaces
|
||||
|
||||
@staticmethod
|
||||
@ -361,63 +235,26 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if 'uptrend_long_ema' in params and params['uptrend_long_ema']['enabled']:
|
||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||
if 'macd_below_zero' in params and params['macd_below_zero']['enabled']:
|
||||
conditions.append(dataframe['macd'] < 0)
|
||||
if 'uptrend_short_ema' in params and params['uptrend_short_ema']['enabled']:
|
||||
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
||||
if 'mfi' in params and params['mfi']['enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||
if 'fastd' in params and params['fastd']['enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
||||
if 'adx' in params and params['adx']['enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||
if 'rsi' in params and params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
if 'over_sar' in params and params['over_sar']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||
if 'green_candle' in params and params['green_candle']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['open'])
|
||||
if 'uptrend_sma' in params and params['uptrend_sma']['enabled']:
|
||||
prevsma = dataframe['sma'].shift(1)
|
||||
conditions.append(dataframe['sma'] > prevsma)
|
||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||
if 'fastd-enabled' in params and params['fastd-enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
||||
if 'adx-enabled' in params and params['adx-enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||
|
||||
# TRIGGERS
|
||||
triggers = {
|
||||
'lower_bb': (
|
||||
dataframe['close'] < dataframe['bb_lowerband']
|
||||
),
|
||||
'lower_bb_tema': (
|
||||
dataframe['tema'] < dataframe['bb_lowerband']
|
||||
),
|
||||
'faststoch10': (qtpylib.crossed_above(
|
||||
dataframe['fastd'], 10.0
|
||||
)),
|
||||
'ao_cross_zero': (qtpylib.crossed_above(
|
||||
dataframe['ao'], 0.0
|
||||
)),
|
||||
'ema3_cross_ema10': (qtpylib.crossed_above(
|
||||
dataframe['ema3'], dataframe['ema10']
|
||||
)),
|
||||
'macd_cross_signal': (qtpylib.crossed_above(
|
||||
if params['trigger'] == 'bb_lower':
|
||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||
if params['trigger'] == 'macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macd'], dataframe['macdsignal']
|
||||
)),
|
||||
'sar_reversal': (qtpylib.crossed_above(
|
||||
))
|
||||
if params['trigger'] == 'sar_reversal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['close'], dataframe['sar']
|
||||
)),
|
||||
'ht_sine': (qtpylib.crossed_above(
|
||||
dataframe['htleadsine'], dataframe['htsine']
|
||||
)),
|
||||
'heiken_reversal_bull': (
|
||||
(qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) &
|
||||
(dataframe['ha_low'] == dataframe['ha_open'])
|
||||
),
|
||||
'di_cross': (qtpylib.crossed_above(
|
||||
dataframe['plus_di'], dataframe['minus_di']
|
||||
)),
|
||||
}
|
||||
conditions.append(triggers.get(params['trigger']['type']))
|
||||
))
|
||||
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
@ -427,7 +264,9 @@ class Hyperopt(Backtesting):
|
||||
|
||||
return populate_buy_trend
|
||||
|
||||
def generate_optimizer(self, params: Dict) -> Dict:
|
||||
def generate_optimizer(self, _params) -> Dict:
|
||||
params = self.get_args(_params)
|
||||
|
||||
if self.has_space('roi'):
|
||||
self.analyze.strategy.minimal_roi = self.generate_roi_table(params)
|
||||
|
||||
@ -437,10 +276,11 @@ class Hyperopt(Backtesting):
|
||||
if self.has_space('stoploss'):
|
||||
self.analyze.strategy.stoploss = params['stoploss']
|
||||
|
||||
processed = load(TICKERDATA_PICKLE)
|
||||
results = self.backtest(
|
||||
{
|
||||
'stake_amount': self.config['stake_amount'],
|
||||
'processed': self.processed,
|
||||
'processed': processed,
|
||||
'realistic': self.config.get('realistic_simulation', False),
|
||||
}
|
||||
)
|
||||
@ -450,30 +290,18 @@ class Hyperopt(Backtesting):
|
||||
trade_count = len(results.index)
|
||||
trade_duration = results.trade_duration.mean()
|
||||
|
||||
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
||||
print('.', end='')
|
||||
sys.stdout.flush()
|
||||
if trade_count == 0:
|
||||
return {
|
||||
'status': STATUS_FAIL,
|
||||
'loss': float('inf')
|
||||
'loss': MAX_LOSS,
|
||||
'params': params,
|
||||
'result': result_explanation,
|
||||
}
|
||||
|
||||
loss = self.calculate_loss(total_profit, trade_count, trade_duration)
|
||||
|
||||
self.current_tries += 1
|
||||
|
||||
self.log_results(
|
||||
{
|
||||
'loss': loss,
|
||||
'current_tries': self.current_tries,
|
||||
'total_tries': self.total_tries,
|
||||
'result': result_explanation,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
'loss': loss,
|
||||
'status': STATUS_OK,
|
||||
'params': params,
|
||||
'result': result_explanation,
|
||||
}
|
||||
|
||||
@ -491,6 +319,27 @@ class Hyperopt(Backtesting):
|
||||
results.trade_duration.mean(),
|
||||
)
|
||||
|
||||
def get_optimizer(self, cpu_count) -> Optimizer:
|
||||
return Optimizer(
|
||||
self.hyperopt_space(),
|
||||
base_estimator="ET",
|
||||
acq_optimizer="auto",
|
||||
n_initial_points=30,
|
||||
acq_optimizer_kwargs={'n_jobs': cpu_count}
|
||||
)
|
||||
|
||||
def run_optimizer_parallel(self, parallel, asked) -> List:
|
||||
return parallel(delayed(self.generate_optimizer)(v) for v in asked)
|
||||
|
||||
def load_previous_results(self):
|
||||
""" read trials file if we have one """
|
||||
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
||||
self.trials = self.read_trials()
|
||||
logger.info(
|
||||
'Loaded %d previous evaluations from disk.',
|
||||
len(self.trials)
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
@ -503,67 +352,35 @@ class Hyperopt(Backtesting):
|
||||
|
||||
if self.has_space('buy'):
|
||||
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
|
||||
self.processed = self.tickerdata_to_dataframe(data)
|
||||
dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
|
||||
self.exchange = None # type: ignore
|
||||
self.load_previous_results()
|
||||
|
||||
logger.info('Preparing Trials..')
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
# read trials file if we have one
|
||||
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
||||
self.trials = self.read_trials()
|
||||
|
||||
self.current_tries = len(self.trials.results)
|
||||
self.total_tries += self.current_tries
|
||||
logger.info(
|
||||
'Continuing with trials. Current: %d, Total: %d',
|
||||
self.current_tries,
|
||||
self.total_tries
|
||||
)
|
||||
cpus = multiprocessing.cpu_count()
|
||||
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
|
||||
|
||||
opt = self.get_optimizer(cpus)
|
||||
EVALS = max(self.total_tries//cpus, 1)
|
||||
try:
|
||||
best_parameters = fmin(
|
||||
fn=self.generate_optimizer,
|
||||
space=self.hyperopt_space(),
|
||||
algo=tpe.suggest,
|
||||
max_evals=self.total_tries,
|
||||
trials=self.trials
|
||||
)
|
||||
with Parallel(n_jobs=cpus) as parallel:
|
||||
for i in range(EVALS):
|
||||
asked = opt.ask(n_points=cpus)
|
||||
f_val = self.run_optimizer_parallel(parallel, asked)
|
||||
opt.tell(asked, [i['loss'] for i in f_val])
|
||||
|
||||
results = sorted(self.trials.results, key=itemgetter('loss'))
|
||||
best_result = results[0]['result']
|
||||
|
||||
except ValueError:
|
||||
best_parameters = {}
|
||||
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \
|
||||
'try with more epochs (param: -e).'
|
||||
|
||||
# Improve best parameter logging display
|
||||
if best_parameters:
|
||||
best_parameters = space_eval(
|
||||
self.hyperopt_space(),
|
||||
best_parameters
|
||||
)
|
||||
|
||||
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
|
||||
if 'roi_t1' in best_parameters:
|
||||
logger.info('ROI table:\n%s', self.generate_roi_table(best_parameters))
|
||||
|
||||
logger.info('Best Result:\n%s', best_result)
|
||||
|
||||
# Store trials result to file to resume next time
|
||||
self.save_trials()
|
||||
|
||||
def signal_handler(self, sig, frame) -> None:
|
||||
"""
|
||||
Hyperopt SIGINT handler
|
||||
"""
|
||||
logger.info(
|
||||
'Hyperopt received %s',
|
||||
signal.Signals(sig).name
|
||||
)
|
||||
self.trials += f_val
|
||||
for j in range(cpus):
|
||||
self.log_results({
|
||||
'loss': f_val[j]['loss'],
|
||||
'current_tries': i * cpus + j,
|
||||
'total_tries': self.total_tries,
|
||||
'result': f_val[j]['result'],
|
||||
})
|
||||
except KeyboardInterrupt:
|
||||
print('User interrupted..')
|
||||
|
||||
self.save_trials()
|
||||
self.log_trials_result()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def start(args: Namespace) -> None:
|
||||
|
@ -1,6 +1,5 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||
import os
|
||||
import signal
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@ -40,21 +39,11 @@ def create_trials(mocker) -> None:
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
|
||||
return mocker.Mock(
|
||||
results=[
|
||||
{
|
||||
'loss': 1,
|
||||
'result': 'foo',
|
||||
'status': 'ok'
|
||||
}
|
||||
],
|
||||
best_trial={'misc': {'vals': {'adx': 999}}}
|
||||
)
|
||||
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
||||
|
||||
|
||||
# Unit tests
|
||||
def test_start(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test start() function
|
||||
@ -148,155 +137,18 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
||||
assert caplog.record_tuples == []
|
||||
|
||||
|
||||
def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
|
||||
fmin_result = {
|
||||
"macd_below_zero": 0,
|
||||
"adx": 1,
|
||||
"adx-value": 15.0,
|
||||
"fastd": 1,
|
||||
"fastd-value": 40.0,
|
||||
"green_candle": 1,
|
||||
"mfi": 0,
|
||||
"over_sar": 0,
|
||||
"rsi": 1,
|
||||
"rsi-value": 37.0,
|
||||
"trigger": 2,
|
||||
"uptrend_long_ema": 1,
|
||||
"uptrend_short_ema": 0,
|
||||
"uptrend_sma": 0,
|
||||
"stoploss": -0.1,
|
||||
"roi_t1": 1,
|
||||
"roi_t2": 2,
|
||||
"roi_t3": 3,
|
||||
"roi_p1": 1,
|
||||
"roi_p2": 2,
|
||||
"roi_p3": 3,
|
||||
}
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'epochs': 1})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
||||
patch_exchange(mocker)
|
||||
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt.trials = create_trials(mocker)
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.start()
|
||||
|
||||
exists = [
|
||||
'Best parameters:',
|
||||
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
|
||||
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
|
||||
'"green_candle": {\n "enabled": true\n },',
|
||||
'"macd_below_zero": {\n "enabled": false\n },',
|
||||
'"mfi": {\n "enabled": false\n },',
|
||||
'"over_sar": {\n "enabled": false\n },',
|
||||
'"roi_p1": 1.0,',
|
||||
'"roi_p2": 2.0,',
|
||||
'"roi_p3": 3.0,',
|
||||
'"roi_t1": 1.0,',
|
||||
'"roi_t2": 2.0,',
|
||||
'"roi_t3": 3.0,',
|
||||
'"rsi": {\n "enabled": true,\n "value": 37.0\n },',
|
||||
'"stoploss": -0.1,',
|
||||
'"trigger": {\n "type": "faststoch10"\n },',
|
||||
'"uptrend_long_ema": {\n "enabled": true\n },',
|
||||
'"uptrend_short_ema": {\n "enabled": false\n },',
|
||||
'"uptrend_sma": {\n "enabled": false\n }',
|
||||
'ROI table:\n{0: 6.0, 3.0: 3.0, 5.0: 1.0, 6.0: 0}',
|
||||
'Best Result:\nfoo'
|
||||
]
|
||||
for line in exists:
|
||||
assert line in caplog.text
|
||||
|
||||
|
||||
def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'epochs': 1})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
patch_exchange(mocker)
|
||||
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt.trials = create_trials(mocker)
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
exists = [
|
||||
'Best Result:',
|
||||
'Sorry, Hyperopt was not able to find good parameters. Please try with more epochs '
|
||||
'(param: -e).',
|
||||
]
|
||||
|
||||
for line in exists:
|
||||
assert line in caplog.text
|
||||
|
||||
|
||||
def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, default_conf) -> None:
|
||||
trials = create_trials(mocker)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'epochs': 1})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=True)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.len', return_value=len(trials.results))
|
||||
mock_read = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.read_trials',
|
||||
return_value=trials
|
||||
)
|
||||
mock_save = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.save_trials',
|
||||
return_value=None
|
||||
)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||
patch_exchange(mocker)
|
||||
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt.trials = trials
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
mock_read.assert_called_once()
|
||||
mock_save.assert_called_once()
|
||||
|
||||
current_tries = hyperopt.current_tries
|
||||
total_tries = hyperopt.total_tries
|
||||
|
||||
assert current_tries == len(trials.results)
|
||||
assert total_tries == (current_tries + len(trials.results))
|
||||
|
||||
|
||||
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
|
||||
create_trials(mocker)
|
||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||
trials = create_trials(mocker)
|
||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
mocker.patch('freqtrade.optimize.hyperopt.open', return_value=hyperopt.trials_file)
|
||||
_HYPEROPT.trials = trials
|
||||
|
||||
hyperopt.save_trials()
|
||||
|
||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
assert log_has(
|
||||
'Saving Trials to \'{}\''.format(trials_file),
|
||||
'Saving 1 evaluations to \'{}\''.format(trials_file),
|
||||
caplog.record_tuples
|
||||
)
|
||||
mock_dump.assert_called_once()
|
||||
@ -304,8 +156,7 @@ def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
|
||||
|
||||
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker)
|
||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
|
||||
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
|
||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
hyperopt_trial = hyperopt.read_trials()
|
||||
@ -315,7 +166,6 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert hyperopt_trial == trials
|
||||
mock_open.assert_called_once()
|
||||
mock_load.assert_called_once()
|
||||
|
||||
|
||||
@ -333,12 +183,15 @@ def test_roi_table_generation(init_hyperopt) -> None:
|
||||
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||
|
||||
|
||||
def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
|
||||
trials = create_trials(mocker)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||
def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1))
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
@ -347,11 +200,13 @@ def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
|
||||
conf.update({'spaces': 'all'})
|
||||
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt.trials = trials
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
|
||||
hyperopt.start()
|
||||
mock_fmin.assert_called_once()
|
||||
parallel.assert_called_once()
|
||||
|
||||
assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text
|
||||
assert dumper.called
|
||||
|
||||
|
||||
def test_format_results(init_hyperopt):
|
||||
@ -384,20 +239,6 @@ def test_format_results(init_hyperopt):
|
||||
assert result.find('Total profit 1.00000000 EUR')
|
||||
|
||||
|
||||
def test_signal_handler(mocker, init_hyperopt):
|
||||
"""
|
||||
Test Hyperopt.signal_handler()
|
||||
"""
|
||||
m = MagicMock()
|
||||
mocker.patch('sys.exit', m)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.save_trials', m)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
hyperopt.signal_handler(signal.SIGTERM, None)
|
||||
assert m.call_count == 3
|
||||
|
||||
|
||||
def test_has_space(init_hyperopt):
|
||||
"""
|
||||
Test Hyperopt.has_space() method
|
||||
@ -422,8 +263,8 @@ def test_populate_indicators(init_hyperopt) -> None:
|
||||
|
||||
# Check if some indicators are generated. We will not test all of them
|
||||
assert 'adx' in dataframe
|
||||
assert 'ao' in dataframe
|
||||
assert 'cci' in dataframe
|
||||
assert 'mfi' in dataframe
|
||||
assert 'rsi' in dataframe
|
||||
|
||||
|
||||
def test_buy_strategy_generator(init_hyperopt) -> None:
|
||||
@ -437,44 +278,15 @@ def test_buy_strategy_generator(init_hyperopt) -> None:
|
||||
|
||||
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
|
||||
{
|
||||
'uptrend_long_ema': {
|
||||
'enabled': True
|
||||
},
|
||||
'macd_below_zero': {
|
||||
'enabled': True
|
||||
},
|
||||
'uptrend_short_ema': {
|
||||
'enabled': True
|
||||
},
|
||||
'mfi': {
|
||||
'enabled': True,
|
||||
'value': 20
|
||||
},
|
||||
'fastd': {
|
||||
'enabled': True,
|
||||
'value': 20
|
||||
},
|
||||
'adx': {
|
||||
'enabled': True,
|
||||
'value': 20
|
||||
},
|
||||
'rsi': {
|
||||
'enabled': True,
|
||||
'value': 20
|
||||
},
|
||||
'over_sar': {
|
||||
'enabled': True,
|
||||
},
|
||||
'green_candle': {
|
||||
'enabled': True,
|
||||
},
|
||||
'uptrend_sma': {
|
||||
'enabled': True,
|
||||
},
|
||||
|
||||
'trigger': {
|
||||
'type': 'lower_bb'
|
||||
}
|
||||
'adx-value': 20,
|
||||
'fastd-value': 20,
|
||||
'mfi-value': 20,
|
||||
'rsi-value': 20,
|
||||
'adx-enabled': True,
|
||||
'fastd-enabled': True,
|
||||
'mfi-enabled': True,
|
||||
'rsi-enabled': True,
|
||||
'trigger': 'bb_lower'
|
||||
}
|
||||
)
|
||||
result = populate_buy_trend(dataframe)
|
||||
@ -503,35 +315,34 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
||||
MagicMock(return_value=backtest_result)
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
||||
|
||||
optimizer_param = {
|
||||
'adx': {'enabled': False},
|
||||
'fastd': {'enabled': True, 'value': 35.0},
|
||||
'green_candle': {'enabled': True},
|
||||
'macd_below_zero': {'enabled': True},
|
||||
'mfi': {'enabled': False},
|
||||
'over_sar': {'enabled': False},
|
||||
'roi_p1': 0.01,
|
||||
'roi_p2': 0.01,
|
||||
'roi_p3': 0.1,
|
||||
'adx-value': 0,
|
||||
'fastd-value': 35,
|
||||
'mfi-value': 0,
|
||||
'rsi-value': 0,
|
||||
'adx-enabled': False,
|
||||
'fastd-enabled': True,
|
||||
'mfi-enabled': False,
|
||||
'rsi-enabled': False,
|
||||
'trigger': 'macd_cross_signal',
|
||||
'roi_t1': 60.0,
|
||||
'roi_t2': 30.0,
|
||||
'roi_t3': 20.0,
|
||||
'rsi': {'enabled': False},
|
||||
'roi_p1': 0.01,
|
||||
'roi_p2': 0.01,
|
||||
'roi_p3': 0.1,
|
||||
'stoploss': -0.4,
|
||||
'trigger': {'type': 'macd_cross_signal'},
|
||||
'uptrend_long_ema': {'enabled': False},
|
||||
'uptrend_short_ema': {'enabled': True},
|
||||
'uptrend_sma': {'enabled': True}
|
||||
}
|
||||
|
||||
response_expected = {
|
||||
'loss': 1.9840569076926293,
|
||||
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||
'(0.0231Σ%). Avg duration 100.0 mins.',
|
||||
'status': 'ok'
|
||||
'params': optimizer_param
|
||||
}
|
||||
|
||||
hyperopt = Hyperopt(conf)
|
||||
generate_optimizer_value = hyperopt.generate_optimizer(optimizer_param)
|
||||
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
|
||||
assert generate_optimizer_value == response_expected
|
||||
|
@ -15,11 +15,11 @@ TA-Lib==0.4.17
|
||||
pytest==3.6.2
|
||||
pytest-mock==1.10.0
|
||||
pytest-cov==2.5.1
|
||||
hyperopt==0.1
|
||||
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
||||
networkx==1.11 # pyup: ignore
|
||||
tabulate==0.8.2
|
||||
coinmarketcap==5.0.3
|
||||
|
||||
# Required for hyperopt
|
||||
scikit-optimize==0.5.2
|
||||
|
||||
# Required for plotting data
|
||||
#plotly==2.7.0
|
||||
|
Loading…
Reference in New Issue
Block a user